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.class Last modified Nov 6, 2014; size 1401 bytes MD5 checksum fe2b2d3f039a9ba4209c488a8c4b4ea8 Compiled from "Lambda1.java" public class com.colobu.lambda.chapter5.Lambda1 SourceFile: "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;)V InnerClasses: public static final #68= #64 of #66; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles minor version: 0 major version: 52 flags: 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_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/colobu/lambda/chapter5/Lambda1; public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: invokedynamic #19, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 5: astore_1 6: aload_1 7: ldc #20 // String hello lambda 9: invokeinterface #22, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V 14: return LineNumberTable: line 10: 0 line 11: 6 line 12: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 args [Ljava/lang/String; 6 9 1 c Ljava/util/function/Consumer; LocalVariableTypeTable: Start Length Slot Name Signature 6 9 1 c Ljava/util/function/Consumer<Ljava/lang/String;>; private static void lambda$0(java.lang.String); flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=1, args_size=1 0: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 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
相關文章
- 「Java8系列」神祕的LambdaJava
- Java8-lambdaJava
- [轉]Java 8 的 lambda 表示式 Java 8 的 lambda 表示式Java
- [譯] Kotlin 揭祕:理解並速記 Lambda 語法Kotlin
- 揭祕JAVA JVM內幕JavaJVM
- Java 8 Lambda 表示式Java
- java 8 lambda表示式Java
- Java8新特性 - LambdaJava
- Java8-Lambda表示式Java
- java8 lambda表示式Java
- 掌握 Java 8 Lambda 表示式Java
- 學習Java8系列-LambdaJava
- Java8 Lambda 之 Collection StreamJava
- Java8的Lambda表示式Java
- Java8新特性系列-LambdaJava
- Java8新特性系列(Lambda)Java
- Java 8 中的 lambda 表示式Java
- 深入探索 Java 8 Lambda 表示式Java
- java8特性-lambda表示式Java
- java命令的本質邏輯揭祕Java
- 《Java 8 in Action》Chapter 3:Lambda表示式JavaAPT
- Java 8新特性(一):Lambda表示式Java
- Java8中的Lambda表示式Java
- Java 8 lambda 表示式10個示例Java
- 【譯】java8之lambda表示式Java
- Java 8 之 lambda 變數作用域Java變數
- Java 8 流特性和 Lambda 表示式Java
- 揭祕Java高效隨機數生成器Java隨機
- 揭祕ThreadLocalthread
- 揭祕instancetype
- Java8新特性(1):Lambda表示式Java
- Java8中的 lambda 和Stream APIJavaAPI
- java8學習:lambda表示式(2)Java
- java8學習:lambda表示式(1)Java
- Java8新特性(一)-Lambda表示式Java
- java8 新特性之Lambda 表示式Java
- Java8 新特性之 Lambda 表示式Java
- Lambda表示式之爭:Scala vs Java 8Java