標籤:java 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運算式的。