Java 람다의 실체
Java에 람다 표현식이 추가된 지 꽤 됐지만, 람다 표현식이 컴파일러를 통해 익명 클래스로 변환되는 편의 문법 정도로 생각하는 경우가 많은 것 같다. 컴파일러가 생성한 클래스 파일을 살펴보고, 람다 표현식이 실제로 어떻게 컴파일 되는지 확인해보려 한다.
먼저, 간단한 람다 표현식을 포함하는 클래스를 작성해 컴파일해보자.
import java.util.function.IntBinaryOperator;
public class Lambda {
public static void main(String[] args) {
IntBinaryOperator add = (a, b) -> a + b; // lambda
System.out.println(add.applyAsInt(10, 20));
}
}
위와 동일한 코드를 익명 클래스를 사용해 작성하면 다음과 같다.
import java.util.function.IntBinaryOperator;
public class InnerClass {
public static void main(String[] args) {
IntBinaryOperator add = new IntBinaryOperator() { // anonymous class
@Override
public int applyAsInt(int a, int b) {
return a + b;
}
};
System.out.println(add.applyAsInt(10, 20));
}
}
람다 표현식은 함수형 인터페이스의 추상 메서드를 구현하는 것과 동일해 보이므로, 컴파일러가 람다 표현식을 익명 클래스로 변환하는 것이 타당해 보인다. 정말 위 두 코드는 동일한 바이트코드로 컴파일될까?
클래스 파일
익명 클래스가 있는 소스 코드를 컴파일하면 익명 클래스를 위한 별도의 클래스 파일이 생성된다. 위 경우에는 InnerClass$1.class
파일이 생성될 것이다. 람다 표현식이 익명 클래스로 변환된다면 Lambda$1.class
생성되어야 할 것이다. 클래스 파일이 저장된 디렉터리로 가서 클래스 파일을 확인해보자.
$ ls InnerClass$1.class InnerClass.class Lambda.class
예상대로 InnerClass
의 경우는 InnerClass.class
와 함께 InnerClass$1.class
파일이 함께 생성되었다. 그러나 Lambda
의 경우는 Lambda.class
만 생성되었을 뿐 Lambda$1.class
는 보이지 않는다. 람다 표현식은 익명 클래스와 다르게 컴파일되는 것이 분명하다.
InnerClass 바이트코드
javap
명령을 이용하면 클래스 파일의 바이트코드를 살펴볼 수 있다.
$ javap -c -p ClassName
역어셈블된 코드를 보고 싶으면 -c
옵션을 추가해야 한다. -p
옵션을 주면 private
을 포함한 모든 멤버와 클래스를 보여준다.
역어셈블된 InnerClass
의 코드는 다음과 같다. new
연산을 이용해 InnerClass$1
의 인스턴스를 생성하는 코드가 보인다. 또한 invokeinterface
를 이용해 applyAsInt
메서드를 호출하는 코드도 보인다.
Compiled from "InnerClass.java" public class InnerClass { public InnerClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class InnerClass$1 3: dup 4: invokespecial #3 // Method InnerClass$1." ":()V 7: astore_1 8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: bipush 10 14: bipush 20 16: invokeinterface #5, 3 // InterfaceMethod java/util/function/IntBinaryOperator.applyAsInt:(II)I 21: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 24: return }
InnerClass$1
를 역어셈블한 코드는 다음과 같다. InnerClass$1
은 IntBinaryOperator
인터페이스를 구현하는 클래스며 applyAsInt
메서드가 정의되어 있는 것을 확인할 수 있다.
Compiled from "InnerClass.java" class InnerClass$1 implements java.util.function.IntBinaryOperator { InnerClass$1(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public int applyAsInt(int, int); Code: 0: iload_1 1: iload_2 2: iadd 3: ireturn }
Lambda 바이트코드
람다 표현식을 포함한 소스 코드를 컴파일 했을 때 ClassName$1
과 같이 내부 클래스를 컴파일했을 때 생기는 클래스 파일이 생성되지 않음을 이미 확인했다. 람다 표현식은 어떻게 Java 바이트코드로 변환될까?
다음은 javap
로 Lambda
를 역어셈블한 코드다. 익명 클래스의 바이트코드와는 상당히 다르다. 별도 클래스의 인스턴스를 생성하던 부분이 invokedynamic
으로 바뀌어 있는 것을 볼 수 있다. invokedynamic
은 JVM에서 다이나믹 언어를 지원하기 위해 JDK7에 처음 도입된 바이트코드 명령이지만, 여기서는 조금 다른 목적으로 사용되었다.
Compiled from "Lambda.java" public class Lambda { public Lambda(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:applyAsInt:()Ljava/util/function/IntBinaryOperator; 5: astore_1 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_1 10: bipush 10 12: bipush 20 14: invokeinterface #4, 3 // InterfaceMethod java/util/function/IntBinaryOperator.applyAsInt:(II)I 19: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 22: return private static int lambda$main$0(int, int); Code: 0: iload_0 1: iload_1 2: iadd 3: ireturn }
invokedynamic
은 람다 표현식의 바이트코드 변환 전략을 지연하기 위한 용도로 사용되었다. 즉, 람다 표현식 구현을 위한 코드 생성을 실제 런타임까지 연기한다. 이렇게 하면 몇 가지 좋은 점이 있지만, 여기서는 설명하지 않겠다.
이 코드에서는 람다 표현식이 정적 메서드로 변환되었다. 이렇게 외부 상태를 캡쳐하지 않는 람다 표현식은 가장 단순한 형태로, 컴파일러는 람다 표현식을 동일한 시그니처의 메서드로 변환할 수 있다. 람다 표현식이 정적 메서드로 변환된 것은 호출부인 main
메서드가 정적 메서드기 때문이다. main
메서드를 인스턴스 메서드로 바꾸면 람다 표현식을 변환한 메서드 또한 인스턴스 메서드로 바뀔 것이다.
결론
람다 표현식은 익명 클래스의 편의 문법이 아니다. 람다 표현식은 컴파일 시에 익명 클래스로 변환되지 않는다. 대신 invokedynamic
을 이용해 람다 변환 전략을 런타임으로 미루며, 런타임은 람다 표현식을 평가할 때 적절한 전략을 동적으로 선택할 수 있다.
참고
- Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft. (2015) Java 8 in Action: Lambdas, streams, and functional-style programming, Appendix D Lambdas and JVM bytecode, Manning
- Translation of Lambda Expressions