함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. - 위키백과
함수형 프로래밍은 선언적 프로그래밍의 일종, 람다를 지원하기 전 Java는 완전한 명령형 프로그래밍
: 상태를 변경하지 않는 것
자세한건 여기를
: 같은 입력시 같은 출력을 보장한다. 부수 효과(Side Effect)가 없다
Side Effect란?
함수에 예상할 수 없는 일이 생길 가능성이 존재하는 경우를 말한다
반환 값 이외에, 함수 외부의 상태에 영향을 미치는 것
// doSomething 메서드는 state 값에 영향을 받는다 = side effect 존재
public class SideEffectClass {
private int state = 0;
public void doSomething(int arg0) {
state += arg0;
}
}
: 함수를 인자로 받거나 반환하는 함수
// 고차함수 map은 배열의 각 요소에 대해 주어진 함수를 실행함
// 이와 같은 고차함수는 코드의 재사용을 높이고(함수를 값으로 전달할 수 있어서) 간결한 코드를 만든다.
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<Integer> doubledNumbers = numbers.stream().map(x -> x * 2).collect(Collectors.toList());
System.out.println(doubledNumbers); // [2, 4, 6]
: 표현식의 평가를 그 값이 필요할 때까지 지연시키고 반복된 평가를 피하는 전략
import java.util.stream.Stream;
public class LazyEvaluationExample {
public static void main(String[] args) {
Stream<Integer> infiniteStream = Stream.iterate(0, i -> i + 1);
// 지연 연산
// 실제로 이 연산은 최종 연산인 limit과 forEach가 호출될 때까지 지연된다
Stream<Integer> evenNumbersStream = infiniteStream.filter(i -> i % 2 == 0);
// 최종 연산
// 10개의 짝수만 필요하므로 나머지 모든 요소에 대한 필터링 연산이 필요가 없다
evenNumbersStream.limit(10).forEach(System.out::println);
}
}
peek
, sorted
, distinct
같은) 예기치 않은 결과 발생 가능public class LazyEvaluation {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> stream = numbers.stream()
.peek(n -> System.out.println("Peek " + n))
.filter(n -> n % 2 == 0);
System.out.println("Stream created.");
stream.forEach(n -> System.out.println("Found " + n));
}
}
/*
filter에 걸러진 요소만 출력되지 않는다. (예기치 않은 결과)
Peek 1
Peek 2
Found 2
Peek 3
Peek 4
Found 4
Peek 5
Peek 6
Found 6
Peek 7
Peek 8
Found 8
Peek 9
Peek 10
Found 10
*/
: 식별자 없이 실행가능한 함수
람다식의 도입으로 자바는 객체지향언어인 동시에 함수형 언어가 되었다
익명 함수란?
함수의 이름이 없는 함수
1급함수의 특징을 가진다.
그러면 Java는 완전한 함수형 언어가 된 것인가?
NO.
Java는 여전히 객체 지향 언어로서 상태와 가변 데이터를 다루는게 가능하다.(불변x)
따라서 Java는 객체 지향 언어와 함수형 언어의 특징을 모두 가진 멀티패러다임 언어
Q. 메서드와 함수의 차이
전통적으로 프로그래밍에서 함수라는 이름은 수학에서 따온 것
그러나 객체지향개념에서는 함수(function) 대신 객체의 행위나 동작을 의미하는 메서드(method) 라는 용어를 사용하는데
메서드는 함수와 같은 의미이지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미를 다른 용어를 선택해서 사용했다.
그러나 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다.
람다는
1. 매개변수
2. 화살표 '->'
3. 함수몸체
세가지를 이용하여 사용한다
// before
int max(int a, int b) {
return a > b ? a : b;
}
// lambda
(int a, int b) -> { return a > b ? a : b; }
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
Runnable runnable = () -> { };
// 매개변수 1개
a -> a * a // OK
// 매개변수 2개 이상
(a, b) -> a * b // OK
// 매개변수 타입이 있는 경우 () 생략불가
int a -> a * a // Error
: 추상 메소드만을 가진 인터페이스
@FunctionalInterface
로 두 개 이상의 추상 메서드 선언이 안되도록 컴파일 체킹추상 메서드가 하나여야만 하는 이유?
Java의 람다식은 함수형 인터페이스에서만 사용가능한데, 예를들어 함수형 인터페이스에 추상 메서드가 두 개 이상 있으면 람다 표현식으로 어떤 메서드를 구현해야 하는지 불분명해지고 람다 표현식을 만들고 사용하는 구문이 더 복잡해진다
아래 코드처럼 직접 함수형 인터페이스를 정의하고 람다식을 이용해 필요에 따라 구현 가능
@FunctionalInterface
interface MyFunction {
int plus(int x, int y);
}
@FunctionalInterface
interface MyFunction {
int plus(int x, int y);
}
public class AnonymousClassExample {
public static void main(String[] args) {
MyFunction myFunction = new MyFunction() {
@Override
public int plus(int x, int y) {
return x + y;
}
};
int result = myFunction.plus(3, 4);
System.out.println(result); // 7
}
}
// 간결해진다
public class LambdaExample {
public static void main(String[] args) {
MyFunction myFunction = (x, y) -> x + y;
int result = myFunction.plus(3, 4);
System.out.println(result); // 7
}
}
1. 익명 내부 클래스는 새로운 클래스를 생성하지만, 람다는 새로운 메서드를 생성하여 포함
2. this
name | value | 설명 |
---|---|---|
Runnable | 매개 값 X, 리턴 값 X | 쓰레드 |
Consumer | 매개 값 O, 리턴 값 X | 입력값은 존재하는데 내주는게 없다 |
Supplier | 매개 값 X, 리턴 값 O | 입력값은 존재하지않는데, 원하는게 미리 준비됨 |
Function | 매개 값 O, 리턴 값 O - 주로 매개 값을 리턴 값으로 매핑(타입변환) | 값을 변환 |
Operator | 매개 값 O, 리턴 값 X - 주로 매개값을 연산하고 결과를 리턴 | 연산 |
Predicate | 매개 값 O, 리턴 값 X - 매개 값을 검사하여 boolean 리턴 | 참/거짓 판단 |
: 타겟 타이핑(Target Typing)
자세한건 여기
Java 컴파일러는 람다식의 매개변수 타입을 추론하는 과정에서 다음과 같은 단계를 거친다
// sort 메서드
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
List<String> list = Arrays.asList("B", "A", "C");
// Comparator 인터페이스를 사용하여 람다식의 타겟 타입 결정
// compareTo 메서드는 String 클래스의 메소드
list.sort((s1, s2) -> s1.compareTo(s2));
System.out.println(list); // ["A", "B", "C"]
람다식이 사용되는 컨텍스트를 분석해 해당 메서드의 시그니처(정의)를 분석한다. 이 경우 sort
메서드의 정의를 확인한다.
sort
메소드의 첫 번째 파라미터인 Comparator<? super E> c
가 기대하는 대상 형식(target type)을 나타낸다
Comparator
인터페이스는 int compare(T o1, T o2)
라는 하나의 추상 메서드를 정의하는 함수형 인터페이스
compare
메서드는 두 개의 String
객체를 받아 int
값을 반환하는 함수 디스크립터를 묘사
따라서 sort
메서드로 전달된 인수는 이와 같은 요구사항을 만족해야함
전달되는 인수인 (s1, s2) -> s1.compareTo(s2)는 (String s1, String s2) -> int의 형태를 띄고 있기에 유효하며 이렇게 형식 검사는 성공적으로 완료
함수 디스크립터(Function Descriptor)란?
함수형 인터페이스의 추상 메서드 시그니처를 묘사하는데 함수형 인터페이스는 하나의 추상 메서드만을 가지므로 해당 추상 메서드의 시그니처가 함수 디스크립더가 된다
ex)Comparator
의 경우 추상 메서드int compare(T o1, T o2)
의 시그니처인(T o1, T o2) -> int
의 형태를 람다식은 가져야한다
람다식은 Java 8에서 도입된 강력한 기능 중 하나로, 코드를 더욱 간결하고 가독성 있게 만들어준다. 그러나 람다식을 사용할 때 주의해야 할 점도 있는데, 그 중 하나가 변수 캡처(Variable Capture)
변수 캡처란 람다식 내부에서 람다식 외부에 선언된 변수를 참조하는 것을 말한다.
이는 매우 유용한 기능이지만 잘못 사용한다면 예기치 않은 결과가 발생할 수 있다.
final
로 선언되어야함public class VariableCaptureExample {
public static void main(String[] args) {
String text = "Hello World";
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(text); // 컴파일 오류
}
};
runnable.run();
}
}
final
로 선언되지 않아도 된다. 다만, 해당 변수가 Effective Final이어야 하는데. // 해당 코드는 로컬 변수 i가 final하지 않아 컴파일 에러 발생함
public static void main(String[] args) {
List<Runnable> runnables = new ArrayList<>();
for (int i = 0; i < 3; i++) {
runnables.add(() -> System.out.println(i));
}
for (Runnable runnable : runnables) {
runnable.run();
}
}
// j 변수를 만들어 해결한다 (각 람다식 생성마다 새로 생성되기 때문)
public static void main(String[] args) {
List<Runnable> runnables = new ArrayList<>();
for (int i = 0; i < 3; i++) {
int j = i;
runnables.add(() -> System.out.println(j));
}
for (Runnable runnable : runnables) {
runnable.run();
}
}
왜 이런 제약이 있을까?
로컬 변수는 쓰레드끼리 공유가 안되며 스택에 저장되는데, 이 경우 람다식이 실행하는 동안 지역 변수가 스택에서 사라진다면 문제가 발생할 수 있다.
따라서 람다식에서 로컬 변수의 값을 직접 참조하는게 아닌 복사된 값을 사용한다.
이러한 이유로 로컬 변수(매개변수도 마찬가지)는 final이거나 Effective Final이어야한다.
public class LambdaExample {
int instanceVar = 10; // 인스턴스 변수(객체 변수)
public void lambdaMethod() {
int localVar = 20; // 로컬 변수
Runnable r = () -> {
System.out.println("localVar: " + localVar); // 로컬 변수 캡처
System.out.println("instanceVar: " + instanceVar); // 인스턴스 변수 캡처
instanceVar++; // 인스턴스 변수 값 변경 가능
};
r.run();
}
public static void main(String[] args) {
new LambdaExample().lambdaMethod();
}
}
import java.util.function.Consumer;
public class LambdaExample {
public void lambdaMethod(int x) { // 매개변수
Consumer<Integer> c = (y) -> {
System.out.println("x: " + x); // 매개변수 캡처
System.out.println("y: " + y);
};
c.accept(10);
}
public static void main(String[] args) {
new LambdaExample().lambdaMethod(20);
}
}
public class LambdaExample {
static int staticVar = 10; // static 변수
public void lambdaMethod() {
Runnable r = () -> {
System.out.println("staticVar: " + staticVar); // static 변수 캡처
staticVar++; // static 변수 값 변경 가능
};
r.run();
}
public static void main(String[] args) {
new LambdaExample().lambdaMethod();
}
}
: 함수와 그 함수가 선언된 어휘적 환경(Lexical Environment)의 조합
public class ClosureExample {
public static void main(String[] args) {
IntUnaryOperator add5 = makeAdder(5);
IntUnaryOperator add10 = makeAdder(10);
System.out.println(add5.applyAsInt(3)); // 8
System.out.println(add10.applyAsInt(3)); // 13
}
public static IntUnaryOperator makeAdder(int x) {
return y -> x + y;
}
}
makeAdder
함수는 x
(5, 10)라는 매개 변수를 받아서 람다식을 반환y
라는 매개변수(3)를 받아서 x+y
를 반환x
를 참조하기 때문에 클로저: 내부에 선언한 변수로 외부의 변수 값을 덮는 방식
public class ShadowingExample {
int x = 10;
public static void main(String[] args) {
ShadowingExample outer = new ShadowingExample();
outer.method();
}
void method() {
Runnable runnable = new Runnable() {
int x = 20;
@Override
public void run() {
int x = 30;
System.out.println(x); // 30
System.out.println(this.x); // 20
System.out.println(ShadowingExample.this.x); // 10
}
};
runnable.run();
}
}
public static void main(String[] args) {
int x = 10;
Runnable runnable = () -> {
int x = 20;
// 컴파일 에러 발생: Variable 'x' is already defined in the scope
System.out.println(x);
};
runnable.run();
}
@FunctionalInterface
interface ConverterInterface {
String convert(Integer number);
}
public class MethodReferenceSample {
public static void main(String[] args) {
convert(100, (number) -> String.valueOf(number));
convert(100, String::valueOf);
}
public static String convert(Integer number, ConverterInterface converterInterface) {
return converterInterface.convert(number);
}
}
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
// 생성자 메소드를 호출하는 람다식 Constructor Reference
Function<String, Person> personFactory = Person::new;
// Function 인터페이스의 apply 메소드를 호출하여 새로운 Person 객체를 생성
Person person = personFactory.apply("John Doe", 30);
// 생성된 Person 객체의 정보 출력
System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
}
}
public class Application {
public static int square(int x) {
return x * x;
}
public static void main(String[] args) {
// static 메소드를 호출하는 람다식 Static Method Reference
IntUnaryOperator squareFunction = Application::square;
// IntUnaryOperator 인터페이스의 applyAsInt 메소드를 호출하여 값 계산
int result = squareFunction.applyAsInt(5);
// 결과 출력
System.out.println(result); // 25
}
}
(매개변수) -> obj.instanceMethod(매개변수)
obj::instanceMethod
object::toString
() -> object.toString()
불변성
명령형 프로그래밍과 선언적 프로그래밍
타겟타이핑
익명구현클래스 vs 람다
https://velog.io/@yjw8459/Java-Lambda%EB%9E%8C%EB%8B%A4
https://sujl95.tistory.com/76
https://five-cosmos-fb9.notion.site/758e363f9fb04872a604999f8af6a1ae