歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java 8 動態類型語言Lambda表達式實現原理分析

Java 8支持動態語言,看到了很酷的Lambda表達式,對一直以靜態類型語言自居的Java,讓人看到了Java虛擬機可以支持動態語言的目標。

import java.util.function.Consumer;

public class Lambda {
 public static void main(String[] args) {
  Consumer<String> c = s -> System.out.println(s);
  c.accept("hello lambda!");
 }
}

剛看到這個表達式,感覺java的處理方式是屬於內部匿名類的方式

public class Lambda {
 static {
  System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
 }
 public static void main(String[] args) {
  Consumer<String> c = new Consumer<String>(){
   @Override
   public void accept(String s) {
    System.out.println(s);
   }
   };
  c.accept("hello lambda");
 }
}

編譯的結果應該是Lambda.class , Lambda$1.class 猜測在支持動態語言java換湯不換藥,在最後編譯的時候生成我們常見的方式。

但是結果不是這樣的,只是產生了一個Lambda.class

反編譯吧,來看看真相是什麼?

javap -v -p Lambda.class

注意  -p 這個參數 -p 參數會顯示所有的方法,而不帶默認是不會反編譯private 的方法的

  public Lambda();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #21                // Method java/lang/Object."<init>":()V
        4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name  Signature
            0      5    0  this  LLambda;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
        0: invokedynamic #30,  0            // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
        5: astore_1
        6: aload_1
        7: ldc          #31                // String hello lambda
        9: invokeinterface #33,  2          // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
        14: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 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);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic    #46                // Field java/lang/System.out:Ljava/io/PrintStream;
        3: aload_0
        4: invokevirtual #50                // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        7: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name  Signature
            0      8    0    s  Ljava/lang/String;
}
SourceFile: "Lambda.java"
BootstrapMethods:
  0: #66 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:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V
InnerClasses:
    public static final #77= #73 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

在這裡我們發現了幾個與我們常見的java不太一樣的地方,由於常量定義太多了,文章中就不貼出了

1. Invokedynamic 指令

Java的調用函數的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符號引用在靜態類型語言編譯時就能產生,而動態類型語言只有在運行期才能確定接收者類型,改變四大指令的語意對java的版本有很大的影響,所以在JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一個新的指令

Invokedynamic

0: invokedynamic #30,  0            // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

#30 是代表常量#30 也就是後面的注釋InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

0 是占位符號,目前無用

2. BootstrapMethods

每一個invokedynamic指令的實例叫做一個動態調用點(dynamic call site), 動態調用點最開始是未鏈接狀態(unlinked:表示還未指定該調用點要調用的方法), 動態調用點依靠引導方法來鏈接到具體的方法.  引導方法是由編譯器生成, 在運行期當JVM第一次遇到invokedynamic指令時, 會調用引導方法來將invokedynamic指令所指定的名字(方法名,方法簽名)和具體的執行代碼(目標方法)鏈接起來, 引導方法的返回值永久的決定了調用點的行為.引導方法的返回值類型是java.lang.invoke.CallSite, 一個invokedynamic指令關聯一個CallSite, 將所有的調用委托到CallSite當前的target(MethodHandle)

InvokeDynamic #0 就是BootstrapMethods表示#0的位置

  0: #66 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:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V

我們看到調用了LambdaMetaFactory.metafactory 的方法

參數:

LambdaMetafactory.metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType)有六個參數, 按順序描述如下

1. MethodHandles.Lookup caller : 代表查找上下文與調用者的訪問權限, 使用invokedynamic指令時, JVM會自動自動填充這個參數

2. String invokedName : 要實現的方法的名字, 使用invokedynamic時, JVM自動幫我們填充(填充內容來自常量池InvokeDynamic.NameAndType.Name), 在這裡JVM為我們填充為 "apply", 即Consumer.accept方法名.

3. MethodType invokedType : 調用點期望的方法參數的類型和返回值的類型(方法signature). 使用invokedynamic指令時, JVM會自動自動填充這個參數(填充內容來自常量池InvokeDynamic.NameAndType.Type), 在這裡參數為String, 返回值類型為Consumer, 表示這個調用點的目標方法的參數為String, 然後invokedynamic執行完後會返回一個即Consumer實例.

4. MethodType samMethodType :  函數對象將要實現的接口方法類型, 這裡運行時, 值為 (Object)Object 即 Consumer.accept方法的類型(泛型信息被擦除).#67 (Ljava/lang/Object;)V

5. MethodHandle implMethod : 一個直接方法句柄(DirectMethodHandle), 描述在調用時將被執行的具體實現方法 (包含適當的參數適配, 返回類型適配, 和在調用參數前附加上捕獲的參數), 在這裡為 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.

6. MethodType instantiatedMethodType : 函數接口方法替換泛型為具體類型後的方法類型, 通常和 samMethodType 一樣, 不同的情況為泛型:

比如函數接口方法定義為 void accept(T t)  T為泛型標識, 這個時候方法類型為(Object)Void,  在編譯時T已確定, 即T由String替換, 這時samMethodType就是 (Object)Void, 而instantiatedMethodType為(String)Void.

第4, 5, 6 三個參數來自class文件中的. 如上面引導方法字節碼中Method arguments後面的三個參數就是將應用於4, 5, 6的參數.

  Method arguments:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V

我們來看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();
    }

在buildCallSite的函數中

CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();

函數spinInnerClass 構建了這個內部類,也就是生成了一個Lambda$$Lambda$1/716157500 這樣的內部類,這個類是在運行的時候構建的,並不會保存在磁盤中,如果想看到這個構建的類,可以通過設置環境參數

System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");

會在你指定的路徑 . 當前運行路徑上生成這個內部類

3.靜態類

Java在編譯表達式的時候會生成lambda$0靜態私有類,在這個類裡實現了表達式中的方法塊 system.out.println(s);

private static void lambda$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic    #46                // Field java/lang/System.out:Ljava/io/PrintStream;
        3: aload_0
        4: invokevirtual #50                // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        7: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name  Signature
            0      8    0    s  Ljava/lang/String;

當然了在上一步通過設置的jdk.internal.lambda.dumpProxyClasses裡生成的Lambda$$Lambda$1.class

 public void accept(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
        0: aload_1
        1: checkcast    #15                // class java/lang/String
        4: invokestatic  #21                // Method Lambda.lambda$0:(Ljava/lang/String;)V
        7: return
    RuntimeVisibleAnnotations:
      0: #13()

調用了Lambda.lambda$0靜態函數,也就是表達式中的函數塊

總結

這樣就完成的實現了Lambda表達式,使用invokedynamic指令,運行時調用LambdaMetafactory.metafactory動態的生成內部類,實現了接口,內部類裡的調用方法塊並不是動態生成的,只是在原class裡已經編譯生成了一個靜態的方法,內部類只需要調用該靜態方法。

Copyright © Linux教程網 All Rights Reserved