Java Lambda 表達式源碼分析
Lambda 表達式是什麼?JVM 內部究竟是如何實現 Lambda 表達式的?為什麼要這樣實現?
基本概念
Lambda 表達式
下面的例子中,() -> System.out.println("1")
就是一個 Lambda 表達式。Java 8 中每一個 Lambda 表達式必須有一個函數式介面與之對應。Lambda 表達式就是函數式介面的一個實現。
@Test
public void test0() {
Runnable runnable = () -> System.out.println("1");
runnable.run();
ToIntBiFunction<Integer, Integer> function = (n1, n2) -> n1 + n2;
System.out.println(function.applyAsInt(1, 2));
ToIntBiFunction<Integer, Integer> function2 = Integer::sum;
System.out.println(function2.applyAsInt(1, 2));
}
大致形式就是 (param1, param2, param3, param4…) -> { doing…… };
函數式介面
首先要從 FunctionalInterface 註解講起,詳情見 Annotation Type FunctionalInterface。
An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.
簡單總結一下函數式介面的特徵:
- FunctionalInterface 註解標註一個函數式介面,不能標註類,方法,枚舉,屬性這些。
- 如果介面被標註了 @FunctionalInterface,這個類就必須符合函數式介面的規範。
- 即使一個介面沒有標註 @FunctionalInterface,如果這個介面滿足函數式介面規則,依舊可以被當作函數式介面。
注意:interface 中重寫 Object 類中的抽象方法,不會增加介面的方法數,因為介面的實現類都是 Object 的子類。
我們可以看到 Runnable 介面,裡面只有一個抽象方法 run()
,則這個介面就是一個函數式介面。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
方法引用
所謂方法引用,是指如果某個方法簽名和介面恰好一致,就可以直接傳入方法引用。文章開頭的示例中,下面這塊程式碼就是方法引用。
ToIntBiFunction<Integer, Integer> function2 = Integer::sum;
java.lang.Integer#sum 的實現如下:
public static int sum(int a, int b) {
return a + b;
}
比如我們計算一個 Stream 的和,可以直接傳入 Integer::sum
這個方法引用。
@Test
public void test1() {
Integer sum = IntStream.range(0, 10).boxed().reduce(Integer::sum).get();
System.out.println(sum);
}
上面的程式碼中,為什麼可以直接在 reduce 方法中傳入 Integer::sum
這個方法引用呢?這是因為 reduce 方法的入參就是 BinaryOperator
的函數式介面。
Optional<T> reduce(BinaryOperator<T> accumulator);
BinaryOperator
是繼承自 BiFunction
,定義如下:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}
可以看到,只要是符合 R apply(T t, U u);
的方法引用,都可以傳入 reduce 中。可以是上面程式碼中的 Integer::sum
,也可以是 Integer::max
。
深入實現原理
位元組碼
首先寫 2 個 Lambda 方法:
public class LambdaMain {
public static void main(String[] args) {
new Thread(() -> System.out.println("1")).start();
IntStream.range(0, 5).boxed().filter(i -> i < 3).map(i -> i + "").collect(Collectors.toList());
}
}
之後 javac LambdaMain.java
編譯成位元組碼文件,再通過 javap -p LambdaMain
輸出 class 文件的所有類和成員,得到輸出結果:
Compiled from "LambdaMain.java"
public class test.jdk.LambdaMain {
public test.jdk.LambdaMain();
public static void main(java.lang.String[]);
private static java.lang.String lambda$main$2(java.lang.Integer);
private static boolean lambda$main$1(java.lang.Integer);
private static void lambda$main$0();
}
- 輸出的
void lambda$main$0()
對應的是() -> System.out.println("1")
- 輸出的
boolean lambda$main$1(java.lang.Integer)
對應的是i -> i < 3
- 輸出的
java.lang.String lambda$main$2(java.lang.Integer)
對應的是i -> i + ""
我們可以看出 Lambda 表達式在 Java 8 中首先會生成一個私有的靜態函數
。
為什麼不使用匿名內部類?
如果要在 Java 語言中實現 lambda 表達式,生成匿名內部類就可以輕鬆實現。但是 JDK 為什麼沒有這麼實現呢?這是因為匿名內部類有一些缺點。
- 每個匿名內部類都會在
編譯時
創建一個對應的class 文件
,在運行時
不可避免的會有載入、驗證、準備、解析、初始化等類載入
過程。 - 每次調用都會創建一個這個
匿名內部類 class 的實例對象
,無論是有狀態的(使用到了外部的變數)還是無狀態(沒有使用外部變數)的內部類。
invokedynamic
本來要寫文字的,但是俺發現俺總結的思維導圖還挺清晰的,直接提出來吧,囧。
詳情見 Class LambdaMetafactory 官方文檔,java.lang.invoke.LambdaMetafactory#metafactory 的實現。
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
其主要的概念有如下幾個:
- invokedynamic 位元組碼指令:運行時 JVM 第一次到某個地方的這個指令的時候會進行 linkage,會調用用戶指定的 Bootstrap Method 來決定要執行什麼方法,之後便不需要這個步驟。
- Bootstrap Method: 用戶可以自己編寫的方法,最終需要返回一個 CallSite 對象。
- CallSite: 保存 MethodHandle 的容器,裡面有一個 target MethodHandle。
MethodHandle: 真正要執行的方法的指針。
測試一下 Lambda 函數生成的位元組碼,為了方便起見,java 程式碼改成如下:
public class LambdaMain {
public static void main(String[] args) {
new Thread(() -> System.out.println("1")).start();
}
}
先編譯成 class 文件,之後再反彙編 javap -c -p LambdaMain
看下輸出:
Compiled from "LambdaMain.java"
public class test.jdk.LambdaMain {
public test.jdk.LambdaMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
private static void lambda$main$0();
Code:
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String 1
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到 Thread 里的 Runnable 實現是通過 invokedynamic 調用的。詳細情況 JVM 虛擬機規範,等有時間再補充吧~~~
總結
- Lambda 表達式在 Java 中最終編譯成
私有的靜態函數
,JDK 最終使用 invokedynamic 位元組碼指令調用。
參考鏈接
GitHub 項目
Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~
原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。