Java 8 Lambda 揭祕

鳥窩發表於2016-04-23

再瞭解了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();
    }

實際是由InnerClassLambdaMetafactorybuildCallSite來生成。 生成之前會呼叫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

相關文章