Java8學習筆記(4) -- Lambda表達式實現方式

前幾篇文章討論了函數式接口和Lambda表達式語法invokedynamic指令,以及Groovy2如何利用indy指令。本篇文章在前面幾篇的基礎之上,簡要介紹Java8底層是如何實現Lambda表達式的。

示例代碼

本文將以下面的代碼爲例展開討論:

import java.util.Arrays;
import java.util.List;

public class LambdaImplTest {
    
    public static void main(String[] args) {
        m1(Arrays.asList(args));
    }
    
    public static void m1(List<String> list) {
        list.sort((a, b) -> a.length() - b.length());
    }
    
}

插入invokedynamic指令

直接利用匿名類來實現Lambda表達式肯定也是可行的,這樣,Lambda表達式就只是Java編譯器的語法糖而已。但是Java8並沒有這樣做,而是使用了更復雜(也更靈活)的方式:利用indy指令。顯然,這種方式需要編譯器和JVM一同配合來實現。編譯器會在每個Lambda表達式出現的地方插入一條indy指令,同時還必須在class文件裏生成相應的CONSTANT_InvokeDynamic_info常量池項BootstrapMethods屬性等信息。這些信息將這條indy指令的bootstrap方法指向LambdaMetafactory.metafactory(...)方法。

插入lambda$x&y方法

javac編譯LambdaImplTest.java,然後用javap -v -p反編譯.class文件,可以看到,編譯器生成了一個lambda$m1$0方法,並且將Lambda表達式的內容搬到了裏面:

編譯器會按照一定的規則來給Lambda表達式生成方法,以保證這些方法不會重名。如果把字節碼還原成Java代碼的話,LambdaImplTest看起來會像下面這樣:

public class LambdaImplTest {
    
    public static void main(String[] args) {
        m1(Arrays.asList(args));
    }
    
    public static void m1(List<String> list) {
        list.sort(/*lambda*/);
    }
    
    private static int lambda$m1$0(String a, String b) {
        return a.length() - b.length();
    }
    
}

LambdaMetafactory.metafactory(...)方法

當JVM第一次執行到這條indy指令的時候,它會找到這條指令相應的bootstrap方法,然後調用該方法,得到一個CallSite。下面是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();
    }
從代碼可以猜到,Java8內部也是以內部類的方式來實現Lambda表達式的。而InnerClassLambdaMetafactory的buildCallSite()方法證明了這一點,buildCallSite()方法太長,這裏就不貼代碼了,總之它會調用一個叫做spinInnerClass()的方法,正是這個方法使用字節碼工具在內存中生成了一個類。

觀察spinInnerClass()生成的類

如果我們在啓動JVM的時候設置系統屬性"jdk.internal.lambda.dumpProxyClasses"的話,spinnerClass()方法就會將生成的類保存到文件中,以方便調試。如果我們用下面的命令運行LambdaImplTest,就能在“當前目錄”看到這個類:

java -Djdk.internal.lambda.dumpProxyClasses LambdaImplTest
LambdaImplTest$$Lambda$1.class
同樣可以使用javap命令來反編譯這個class文件,下面是反編譯結果(我自己把javap結果轉成了java文件):

final class LambdaImplTest$$Lambda$1 implements java.util.Comparator {
    
    private LambdaImplTest$$Lambda$1() {

    }

    public int compare(Object a, Object b) {
        return LambdaImplTest.lambda$m1$0:((String) a, (String) b);
    }

}
可見這個內部類實現了Comparator接口,compare()方法只是調用lambda$m1$0()方法而已。繼續分析buildCallSite()方法可知,JVM接着實例化了這個內部類的一個實例,然後創建了一個ConstantCallSite,它的目標MethodHandle指向內部類實例的compare()方法。

示意圖

文字描述很難說清楚問題,下面我畫了一張示意圖:


總結

上面分析了最簡單的Lambda表達式的Java內部實現,如果Lambda表達式捕獲了外部參數的話,情況會稍微複雜一點。不過結論仍然不變:Java8底層也是以內部類的方式來實現Lambda表達式的。


發佈了61 篇原創文章 · 獲贊 72 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章