这是一个有趣的问题,Java 8 Lambda 表达式被编译成了什么?
在了解了Java 8 Lambda的一些基本概念和應用后, 我們會有這樣的一個問題:?Lambda表達式被編譯成了什么?
這是一個有趣的問題,涉及到JDK的具體的實現。本文將介紹OpenJDK對Lambda表達式的轉換細節, 讀者可以了解Java 8 Lambda表達式背景知識。
Lambda表達式的轉換策略
Brian Goetz是Oracle的Java語言架構師, JSR 335(Lambda Expression)規范的lead, 寫了幾篇Lambda設計方面的文章, 其中之一就是Translation of Lambda Expressions。這篇文章介紹了Java 8 Lambda設計時的考慮以及實現方法。
他提到, Lambda表達式可以通過內部類, method handle, dynamic proxy等方式實現, 但是這些方法各有優劣。真正要實現Lambda表達式, 必須兼顧兩個目標:一是不引入特定策略,以期為將來的優化提供最大的靈活性, 二是保持類文件格式的穩定。通過Java 7中引入的invokedynamic?(JSR 292), 可以很好的兼顧這兩個目標。
invokedynamic?在缺乏靜態類型信息的情況下可以支持有效的靈活的方法調用。主要是為了日益增長的運行在JVM上的動態類型語言, 如Groovy, JRuby。
invokedynamic?將Lambda表達式的轉換策略推遲到運行時, 這也意味著我們現在編譯的代碼在將來的轉換策略改變的情況下也能正常運行。
編譯器在編譯的時候, 會將Lambda表達式的表達式體 (lambda body)脫糖(desugar) 成一個方法,此方法的參數列表和返回類型和lambda表達式一致, 如果有捕獲參數, 脫糖的方法的參數可能會更多一些, 并會產生一個invokedynamic調用, 調用一個call site。
這個call site被調用時會返回lambda表達式的目標類型(functional interface)的一個實現類。這個call site稱為這個lambda表達式的lambda factory。?lambda factory的bootstrap方法是一個標準方法, 叫做lambda metafactory。
編譯器在轉換lambda表達式時, 可以推斷出表達式的參數類型,返回類型以及異常, 稱之為natural signature, 我們將目標類型的方法簽名稱之為lambda descriptor, lambda factory的返回對象實現了函數式接口, 并且關聯的表達式的代碼邏輯, 稱之為lambda object。
轉換舉例
以上的解釋有點晦澀, 簡單來說
-
編譯時
-
Lambda 表達式會生成一個方法, 方法實現了表達式的代碼邏輯
-
生成invokedynamic指令, 調用bootstrap方法, 由java.lang.invoke.LambdaMetafactory.metafactory方法實現
-
-
運行時
-
invokedynamic指令調用metafactory方法。它會返回一個CallSite, 此CallSite返回目標類型的一個匿名實現類, 此類關聯編譯時產生的方法
-
lambda表達式調用時會調用匿名實現類關聯的方法。
-
最簡單的一個lambda表達式的例子:
public?class?Lambda1?{public?static?void?main(String[]?args)?{Consumer<String>?c?=?s?->?System.out.println(s);c.accept("hello?lambda");} }使用javap查看生成的字節碼?javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class:
[root@colobu?bin]#?javap?-c?-p?-v?com/colobu/lambda/chapter5/Lambda1.class? Classfile?/mnt/eclipse/Lambda/bin/com/colobu/lambda/chapter5/Lambda1.classLast?modified?Nov?6,?2014;?size?1401?bytesMD5?checksum?fe2b2d3f039a9ba4209c488a8c4b4ea8Compiled?from?"Lambda1.java" public?class?com.colobu.lambda.chapter5.Lambda1SourceFile:?"Lambda1.java"BootstrapMethods:0:?#57?invokestatic?java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;Method?arguments:#58?(Ljava/lang/Object;)V#61?invokestatic?com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#62?(Ljava/lang/String;)VInnerClasses:public?static?final?#68=?#64?of?#66;?//Lookup=class?java/lang/invoke/MethodHandles$Lookup?of?class?java/lang/invoke/MethodHandlesminor?version:?0major?version:?52flags:?ACC_PUBLIC,?ACC_SUPER Constant?pool:#1?=?Class??????????????#2?????????????//??com/colobu/lambda/chapter5/Lambda1#2?=?Utf8???????????????com/colobu/lambda/chapter5/Lambda1#3?=?Class??????????????#4?????????????//??java/lang/Object#4?=?Utf8???????????????java/lang/Object#5?=?Utf8???????????????<init>#6?=?Utf8???????????????()V#7?=?Utf8???????????????Code#8?=?Methodref??????????#3.#9??????????//??java/lang/Object."<init>":()V#9?=?NameAndType????????#5:#6??????????//??"<init>":()V#10?=?Utf8???????????????LineNumberTable#11?=?Utf8???????????????LocalVariableTable#12?=?Utf8???????????????this#13?=?Utf8???????????????Lcom/colobu/lambda/chapter5/Lambda1;#14?=?Utf8???????????????main#15?=?Utf8???????????????([Ljava/lang/String;)V#16?=?NameAndType????????#17:#18????????//??accept:()Ljava/util/function/Consumer;#17?=?Utf8???????????????accept#18?=?Utf8???????????????()Ljava/util/function/Consumer;#19?=?InvokeDynamic??????#0:#16?????????//??#0:accept:()Ljava/util/function/Consumer;#20?=?String?????????????#21????????????//??hello?lambda#21?=?Utf8???????????????hello?lambda#22?=?InterfaceMethodref?#23.#25????????//??java/util/function/Consumer.accept:(Ljava/lang/Object;)V#23?=?Class??????????????#24????????????//??java/util/function/Consumer#24?=?Utf8???????????????java/util/function/Consumer#25?=?NameAndType????????#17:#26????????//??accept:(Ljava/lang/Object;)V#26?=?Utf8???????????????(Ljava/lang/Object;)V#27?=?Utf8???????????????args#28?=?Utf8???????????????[Ljava/lang/String;#29?=?Utf8???????????????c#30?=?Utf8???????????????Ljava/util/function/Consumer;#31?=?Utf8???????????????LocalVariableTypeTable#32?=?Utf8???????????????Ljava/util/function/Consumer<Ljava/lang/String;>;#33?=?Utf8???????????????lambda$0#34?=?Utf8???????????????(Ljava/lang/String;)V#35?=?Fieldref???????????#36.#38????????//??java/lang/System.out:Ljava/io/PrintStream;#36?=?Class??????????????#37????????????//??java/lang/System#37?=?Utf8???????????????java/lang/System#38?=?NameAndType????????#39:#40????????//??out:Ljava/io/PrintStream;#39?=?Utf8???????????????out#40?=?Utf8???????????????Ljava/io/PrintStream;#41?=?Methodref??????????#42.#44????????//??java/io/PrintStream.println:(Ljava/lang/String;)V#42?=?Class??????????????#43????????????//??java/io/PrintStream#43?=?Utf8???????????????java/io/PrintStream#44?=?NameAndType????????#45:#34????????//??println:(Ljava/lang/String;)V#45?=?Utf8???????????????println#46?=?Utf8???????????????s#47?=?Utf8???????????????Ljava/lang/String;#48?=?Utf8???????????????SourceFile#49?=?Utf8???????????????Lambda1.java#50?=?Utf8???????????????BootstrapMethods#51?=?Methodref??????????#52.#54????????//??java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;#52?=?Class??????????????#53????????????//??java/lang/invoke/LambdaMetafactory#53?=?Utf8???????????????java/lang/invoke/LambdaMetafactory#54?=?NameAndType????????#55:#56????????//??metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;#55?=?Utf8???????????????metafactory#56?=?Utf8???????????????(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;#57?=?MethodHandle???????#6:#51?????????//??invokestatic?java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;#58?=?MethodType?????????#26????????????//??(Ljava/lang/Object;)V#59?=?Methodref??????????#1.#60?????????//??com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#60?=?NameAndType????????#33:#34????????//??lambda$0:(Ljava/lang/String;)V#61?=?MethodHandle???????#6:#59?????????//??invokestatic?com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#62?=?MethodType?????????#34????????????//??(Ljava/lang/String;)V#63?=?Utf8???????????????InnerClasses#64?=?Class??????????????#65????????????//??java/lang/invoke/MethodHandles$Lookup#65?=?Utf8???????????????java/lang/invoke/MethodHandles$Lookup#66?=?Class??????????????#67????????????//??java/lang/invoke/MethodHandles#67?=?Utf8???????????????java/lang/invoke/MethodHandles#68?=?Utf8???????????????Lookup {public?com.colobu.lambda.chapter5.Lambda1();flags:?ACC_PUBLICCode:stack=1,?locals=1,?args_size=10:?aload_0???????1:?invokespecial?#8??????????????????//?Method?java/lang/Object."<init>":()V4:?return????????LineNumberTable:line?7:?0LocalVariableTable:Start??Length??Slot??Name???Signature0???????5?????0??this???Lcom/colobu/lambda/chapter5/Lambda1;public?static?void?main(java.lang.String[]);flags:?ACC_PUBLIC,?ACC_STATICCode:stack=2,?locals=2,?args_size=10:?invokedynamic?#19,??0?????????????//?InvokeDynamic?#0:accept:()Ljava/util/function/Consumer;5:?astore_1??????6:?aload_1???????7:?ldc???????????#20?????????????????//?String?hello?lambda9:?invokeinterface?#22,??2???????????//?InterfaceMethod?java/util/function/Consumer.accept:(Ljava/lang/Object;)V14:?return????????LineNumberTable:line?10:?0line?11:?6line?12:?14LocalVariableTable:Start??Length??Slot??Name???Signature0??????15?????0??args???[Ljava/lang/String;6???????9?????1?????c???Ljava/util/function/Consumer;LocalVariableTypeTable:Start??Length??Slot??Name???Signature6???????9?????1?????c???Ljava/util/function/Consumer<Ljava/lang/String;>;private?static?void?lambda$0(java.lang.String);flags:?ACC_PRIVATE,?ACC_STATIC,?ACC_SYNTHETICCode:stack=2,?locals=1,?args_size=10:?getstatic?????#35?????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;3:?aload_0???????4:?invokevirtual?#41?????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V7:?return????????LineNumberTable:line?10:?0LocalVariableTable:Start??Length??Slot??Name???Signature0???????8?????0?????s???Ljava/lang/String; }可以看到, Lambda表達式體被生成一個稱之為lambda$0的方法。看字節碼知道它調用System.out.println輸出傳入的參數。
原lambda表達式處產生了一條invokedynamic #19, 0。它會調用bootstrap方法。
0:?#57?invokestatic?java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;Method?arguments:#58?(Ljava/lang/Object;)V#61?invokestatic?com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#62?(Ljava/lang/String;)V如果Lambda表達式寫成Consumer<String> c = (Consumer<String> & Serializable)s -> System.out.println(s);, 則BootstrapMethods的字節碼為
BootstrapMethods:0:?#108?invokestatic?java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;Method?arguments:#109?(Ljava/lang/Object;)V#112?invokestatic?com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#113?(Ljava/lang/String;)V#114?1它調用的是LambdaMetafactory.altMetafactory,和上面的調用的方法不同。#114 1意味著要實現Serializable接口。
如果Lambda表達式寫成``,則BootstrapMethods的字節碼為
BootstrapMethods:0:?#57?invokestatic?java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;Method?arguments:#58?(Ljava/lang/Object;)V#61?invokestatic?com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V#62?(Ljava/lang/String;)V#63?2#64?1#65?com/colobu/lambda/chapter5/ABC#63 2意味著要實現額外的接口。#64 1意味著要實現額外的接口的數量為1。
字節碼的指令含義可以參考這篇文章:Java bytecode instruction listings。
可以看到, Lambda表達式具體的轉換是通過java.lang.invoke.LambdaMetafactory.metafactory實現的, 靜態參數依照lambda表達式和目標類型不同而不同。
LambdaMetafactory.metafactory
現在我們可以重點關注以下?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();}實際是由InnerClassLambdaMetafactory的buildCallSite來生成。生成之前會調用validateMetafactoryArgs方法校驗目標類型(SAM)方法的參數/和產生的方法的參數/返回值類型是否一致。
metaFactory方法的參數:
-
caller: 由JVM提供的lookup context
-
invokedName: JVM提供的NameAndType
-
invokedType: JVM提供的期望的CallSite類型
-
samMethodType: 函數式接口定義的方法的簽名
-
implMethod: 編譯時產生的那個實現方法
-
instantiatedMethodType: 強制的方法簽名和返回類型, 一般和samMethodType相同或者是它的一個特例
上面的代碼基本上是InnerClassLambdaMetafactory.buildCallSite的包裝,下面看看這個方法的實現:
CallSite?buildCallSite()?throws?LambdaConversionException?{final?Class<?>?innerClass?=?spinInnerClass();if?(invokedType.parameterCount()?==?0)?{.....?//調用構造函數初始化一個SAM的實例return?new?ConstantCallSite(MethodHandles.constant(samBase,?inst));}?else?{UNSAFE.ensureClassInitialized(innerClass);return?new?ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP.findStatic(innerClass,?NAME_FACTORY,?invokedType));}}其中spinInnerClass調用asm框架動態的產生SAM的實現類, 這個實現類的的方法將會調用編譯時產生的那個實現方法。你可以在編譯的時候加上參數-Djdk.internal.lambda.dumpProxyClasses, 這樣編譯的時候會自動產生運行時spinInnerClass產生的類。你可以訪問OpenJDK的bug系統了解這個功能。?JDK-8023524
重復的lambda表達式
下面的代碼中,在一個循環中重復生成調用lambda表達式,只會生成同一個lambda對象, 因為只有同一個invokedynamic指令。
for?(int?i?=?0;?i<100;?i++){Consumer<String>?c?=?s?->?System.out.println(s);System.out.println(c.hashCode()); }但是下面的代碼會生成兩個lambda對象, 因為它會生成兩個invokedynamic指令。
Consumer<String>?c?=?s?->?System.out.println(s); System.out.println(c.hashCode()); Consumer<String>?c2?=?s?->?System.out.println(s); System.out.println(c2.hashCode());生成的類名
既然LambdaMetafactory會使用asm框架生成一個匿名類, 那么這個類的類名有什么規律的。
Consumer<String>?c?=?s?->?System.out.println(s); System.out.println(c.getClass().getName()); System.out.println(c.getClass().getSimpleName()); System.out.println(c.getClass().getCanonicalName());輸出結果如下:
com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680 Lambda3$$Lambda$1/640070680 com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680類名格式如 <包名>.<類名>$$Lambda$/. number是由一個計數器生成counter.incrementAndGet()。后綴/<NN>中的數字是一個hash值, 那就是類對象的hash值c.getClass().hashCode()。在Klass::external_name()中生成。
sprintf(hash_buf,?"/"?UINTX_FORMAT,?(uintx)hash);直接調用生成的方法
上面提到, Lambda表達式體會由編譯器生成一個方法,名字格式如Lambda$XXX。既然是類中的實實在在的方法,我們就可以直接調用。當然, 你在代碼中直接寫lambda$0()編譯通不過, 因為Lambda表達式體還沒有被抽取成方法。但是在運行中我們可以通過反射的方式調用。下面的例子使用發射和MethodHandle兩種方式調用這個方法。
public?static?void?main(String[]?args)?throws?Throwable?{Consumer<String>?c?=?s?->?System.out.println(s);Method?m?=?Lambda4.class.getDeclaredMethod("lambda$0",?String.class);m.invoke(null,?"hello?reflect");MethodHandle?mh?=?MethodHandles.lookup().findStatic(Lambda4.class,?"lambda$0",?MethodType.methodType(void.class,?String.class));mh.invoke("hello?MethodHandle"); }捕獲的變量等價于'final'
我們知道,在匿名類中調用外部的參數時,參數必須聲明為final。Lambda體內也可以引用上下文中的變量,變量可以不聲明成final的,但是必須等價于final。下面的例子中變量capturedV等價與final, 并沒有在上下文中重新賦值。
public?class?Lambda5?{String?greeting?=?"hello";public?static?void?main(String[]?args)?throws?Throwable?{Lambda5?capturedV?=?new?Lambda5();Consumer<String>?c?=?s?->?System.out.println(capturedV.greeting?+?"?"?+?s);c.accept("captured?variable");//capturedV?=?null;?//Local?variable?capturedV?defined?in?an?enclosing?scope?must?be?final?or?effectively?final//capturedV.greeting?=?"hi";} }如果反注釋capturedV = null;編譯出錯,因為capturedV在上下文中被改變。但是如果反注釋capturedV.greeting = "hi";?則沒問題, 因為capturedV沒有被重新賦值, 只是它指向的對象的屬性有所變化。
方法引用
public?static?void?main(String[]?args)?throws?Throwable?{Consumer<String>?c??=?System.out::println;c.accept("hello"); }這段代碼不會產生一個類似"Lambda$0"新方法。因為LambdaMetafactory會直接使用這個引用的方法。
BootstrapMethods:0:?#51?invokestatic?java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;Method?arguments:#52?(Ljava/lang/Object;)V#59?invokevirtual?java/io/PrintStream.println:(Ljava/lang/String;)V#60?(Ljava/lang/String;)V#59指示實現方法為System.out::println
總結
以上是生活随笔為你收集整理的这是一个有趣的问题,Java 8 Lambda 表达式被编译成了什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jar 包又冲突了?如何快速确定与哪个
- 下一篇: 面试常考的树,我这样讲给你听!