람다식(Lambda expression)은 메서드를 하나의 ‘식(expression)’으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 ‘익명 함수(anonymous function)’라고도 한다.
모든 메서드는 클래스에 포함되어야 하므로 클래스를 새로 만들어야 하고, 객체도 생성해야 비로소 메서드를 호출할 수 있다. 그러나 람다식은 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있다.
람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다.
람다식은 ‘익명 함수’답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 { } 사이에 ->를 추가한다.
반환타입 메서드이름(매개변수 선언) {
문장들
}
⇩
(매개변수 선언) -> {
문장들
}
람다식은 익명 클래스의 객체와 동등하다.
타입 f = (int a, int b) -> a > b ? a : b;
f의 타입은 참조형이니 클래스 또는 인터페이스가 가능하다. 그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다. 그래야 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.
예를 들어 max()라는 메서드가 정의된 MyFunction 인터페이스가 아래와 같이 정의되어 있다.
interface MyFunction {
public abstract int max(int a, int b);
}
이는 다음과 같이 생성할 수 있다.
MyFunction f = new MyFunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
};
int big = f.max(5, 3);
MyFunction 인터페이스에 정의된 메서드 max()는 람다식 (int a, int b) -> a > b ? a : b과 메서드의 선언부가 일치한다. 따라서 익명 객체를 람다식으로 대체할 수 있다.
MyFunction f = (int a, int b) -> a > b ? a : b;
int big = f.max(5, 3);
이처럼 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.
하나의 메서드가 선언된 인터페이스를 정의하여 람다식을 다루면 기존의 자바 규칙을 어기지 않으면서 자연스럽게 다룰 수 있다. 그래서 인터페이스를 통해 람다식을 다루기로 결정했고, 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스(functional interface)’라고 부른다.
@FunctionalInterface
interface MyFunction {
public abstract int max(int a, int b);
}
함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 애노테이션 @FunctionalInterface를 사용하면 컴파일러가 올바르게 정의되어 있는지 확인해주므로 꼭 사용하도록 하자.
@FunctionalInterface
interface MyFunction {
void myMethod(); // 추상 메서드
}
메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다.
또는 참조변수 없이 직접 람다식을 매개변수로 지정하는 것도 가능하다.
void aMethod(MyFunction f) {
f.myMethod(); // MyFunction에 정의된 메서드 호출
}
...
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
aMethod(() -> System.out.println("myMethod()"));
메서드의 반환타입이 함수형 인터페이스 타입이라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.
MyFunction myMethod() {
MyFunction f = () -> {};
return f;
// return () -> {};
}
람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능하다.
public class Ex01 {
static void execute(MyFunction01 f) {
f.run();
}
static MyFunction01 getMyFuction() {
MyFunction01 f = () -> System.out.println("f3.run()");
return f;
}
public static void main(String[] args) {
MyFunction01 f1 = () -> System.out.println("f1.run()");
MyFunction01 f2 = new MyFunction() {
@Override
public void run() {
System.out.println("f2.run()");
}
};
MyFunction01 f3 = getMyFuction();
f1.run();
f2.run();
f3.run();
execute(f1);
execute( () -> System.out.println("run()") );
}
}
함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐이지 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다.
정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다.
MyFunction f = (MyFunction)( () -> { } );
람다식은 MyFunction 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략가능하다.
람다식은 이름이 없을 뿐 분명히 객체인데도, Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.
Object obj = (Object)( () -> { } ); // error. 함수형 인터페이스로만 가능
굳이 변경하고자 한다면, 함수형 인터페이스로 변환하고 난 후에 가능하다.
public class Ex02 {
public static void main(String[] args) {
MyFunction02 f = () -> {};
Object obj = (MyFunction02)(() -> {});
String str = ((Object)(MyFunction02)(() -> {})).toString();
System.out.println(f);
System.out.println(obj);
System.out.println(str);
// System.out.println( () -> {} );
System.out.println((MyFunction02)( () -> {} ));
// System.out.println((MyFunction02)( () -> {} ).toString());
System.out.println(((Object)(MyFunction02)(() -> {})).toString());
}
}
멤버 메서드 내부에서 클래스의 객체를 생성해 사용할 경우 다음과 같은 문제가 있다.
heap 영역에 생성된 객체가 스택 영역의 변수를 사용하려고 하는데, 해당 시점에 스택 영역에 더 이상 변수가 존재하지 않을 수 있고, 이 때문에 오류가 발생한다.
자바에서는 이 문제를 variable capture라고 하는 값 복사를 사용해 해결한다.
컴파일 시점에 멤버 메소드의 매개변수나 지역 변수를 멤버 메서드 내부에서 생성한 객체가 사용할 경우 객체 내부로 값을 복사해 사용한다. 하지만 모든 값을 복사해서 사용할 수 있는 것은 아니고, final 키워드로 작성되거나 final 성격을 가져야 한다는 제약이 있다.
Local variable은 조건이 final 또는 effectively final이어야 한다.
effectively final은 Java 8에 추가된 syntatic sugar의 일종으로 초기화된 이후 값이 한번도 변경되지 않았다는 것을 말한다. effectively final 변수는 final 키워드가 붙어 있지 않지만 final 키워드를 붙인 것과 동일하게 컴파일에서 처리한다.
Local variable에 이런 조건이 붙어있는 이유는 다음과 같다.
지역 변수는 쓰레드 간에 공유가 불가능하다. 인스턴스 변수는 JVM의 heap 영역에 생성되는데, 지역 변수와 달리 쓰레드 간에 공유가 가능하다. 즉, 지역 변수가 스택에 저장되기 때문에 람다식에서 값을 바로 참조하는데 제약이 있다. 복사된 값을 사용하는데 멀티 쓰레드 환경에서 변경이 되면 동시성에 대한 이슈를 대응하기 힘들기 때문이다.
java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메소드를 함수형 인터페이스로 미리 정의해놨다.
매번 새로운 함수형 인터페이스를 정의하지 않고, 가능하면 이 패키지의 함수형 인터페이스를 사용하자.
기본적인 함수형 인터페이스
매개변수가 두 개인 함수형 인터페이스
UnaryOperator, BinaryOperator
참조