JAVA進階之Lambda實現(xiàn)原理

1、實例解析

先從一個例子開始:

public class LambdaTest {

    public static void print(String name, Print print) {
        print.print(name);
    }

    public static void main(String[] args) {
        String name = "Chen Longfei";
        String prefix = "hello, ";

        print(name, (t) -> System.out.println(t));

        // 與上一行不同的是,Lambda表達式的函數(shù)體中引用了外部變量‘prefix’
        print(name, (t) -> System.out.println(prefix + t));
    }

}

@FunctionalInterface
interface Print {
    void print(String name);
}

例子很簡單,定義了一個函數(shù)式接口Print ,main方法中有兩處代碼以Lambda表達式的方式實現(xiàn)了print接口,分別打印出不帶前綴與帶前綴的名字。

運行程序,打印結果如下:

Chen Longfei
hello, Chen Longfei

(t) -> System.out.println(t)(t) -> System.out.println(prefix + t))之類的Lambda表達式到底是怎樣被編譯與調(diào)用的呢?

我們知道,編譯器編譯Java代碼時經(jīng)常在背地里“搞鬼”比如類的全限定名的補全,泛型的類型推斷等,編譯器耍的這些小聰明可以幫助我們寫出更優(yōu)雅、簡潔、高效的代碼。鑒于編譯器的一貫作風,我們有理由懷疑,新穎而另類的Lambda表達式在編譯時很可能會被改造過了。

下面通過javap反編譯class文件一探究竟。

javap是jdk自帶的一個字節(jié)碼查看工具及反編譯工具:

用法: javap <options> <classes>

其中, 可能的選項包括:

  -help  --help  -?        輸出此用法消息
  -version                 版本信息
  -v  -verbose             輸出附加信息
  -l                       輸出行號和本地變量表
  -public                  僅顯示公共類和成員
  -protected               顯示受保護的/公共類和成員
  -package                 顯示程序包/受保護的/公共類
                           和成員 (默認)
  -p  -private             顯示所有類和成員
  -c                       對代碼進行反匯編
  -s                       輸出內(nèi)部類型簽名
  -sysinfo                 顯示正在處理的類的
                           系統(tǒng)信息 (路徑, 大小, 日期, MD5 散列)
  -constants               顯示最終常量
  -classpath <path>        指定查找用戶類文件的位置
  -cp <path>               指定查找用戶類文件的位置
  -bootclasspath <path>    覆蓋引導類文件的位置

javap -p Print.class

結果如下:

interface test.Print {
  public abstract void print(java.lang.String);
}

javap -p LambdaTest.class

結果如下:

    // Compiled from "LambdaTest.java"
    public class test.LambdaTest
    {
      public test.LambdaTest();

        public static void print(java.lang.String, test.Print);

        public static void main(java.lang.String[]);

        private static void Lambda$main$1(java.lang.String);

        private static void Lambda$main$0(java.lang.String, java.lang.String);
    }

可見,編譯器對Print接口的改造比較小,只是為print方法添加了public abstract關鍵字,而對LambdaTest的變化就比較大了,添加了兩個靜態(tài)方法:

private static void Lambda$main$1(java.lang.String);

private static void Lambda$main$0(java.lang.String, java.lang.String);

對比原生的java代碼,很容易做出推測,這兩個靜態(tài)方法與兩處Lambda表達式相關:

print(name, (t) -> System.out.println(t));
print(name, (t) -> System.out.println(prefix + t));

到底有什么關聯(lián)呢?使用javap -p -v -c LambdaTest.class查看更加詳細的反編譯結果:

public class test.LambdaTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:                           
   #1 = Methodref          #15.#30        // java/lang/Object."<init>":()V
   #2 = InterfaceMethodref #31.#32        // test/Print.print:(Ljava/lang/String;)V
   #3 = String             #33            // Chen Longfei
   #4 = String             #34            // hello,
   #5 = InvokeDynamic      #0:#39         // #0:print:(Ljava/lang/String;)Ltest/Print;
   #6 = Methodref          #14.#40        // test/LambdaTest.print:(Ljava/lang/String;Ltest/Print;)V
   #7 = InvokeDynamic      #1:#42         // #1:print:()Ltest/Print;
   #8 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Class              #47            // java/lang/StringBuilder
  #11 = Methodref          #10.#30        // java/lang/StringBuilder."<init>":()V
  #12 = Methodref          #10.#48        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder
;
  #13 = Methodref          #10.#49        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #14 = Class              #50            // test/LambdaTest
  #15 = Class              #51            // java/lang/Object
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               print
  #21 = Utf8               (Ljava/lang/String;Ltest/Print;)V
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               Lambda$main$1
  #25 = Utf8               (Ljava/lang/String;)V
  #26 = Utf8               Lambda$main$0
  #27 = Utf8               (Ljava/lang/String;Ljava/lang/String;)V
  #28 = Utf8               SourceFile
  #29 = Utf8               LambdaTest.java
  #30 = NameAndType        #16:#17        // "<init>":()V
  #31 = Class              #52            // test/Print
  #32 = NameAndType        #20:#25        // print:(Ljava/lang/String;)V
  #33 = Utf8               Chen Longfei
  #34 = Utf8               hello,
  #35 = Utf8               BootstrapMethods
  #36 = MethodHandle       #6:#53         // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/inv
oke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/M
ethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #37 = MethodType         #25            //  (Ljava/lang/String;)V
  #38 = MethodHandle       #6:#54         // invokestatic test/LambdaTest.Lambda$main$0:(Ljava/lang/String;Ljava/lang/St
ring;)V
  #39 = NameAndType        #20:#55        // print:(Ljava/lang/String;)Ltest/Print;
  #40 = NameAndType        #20:#21        // print:(Ljava/lang/String;Ltest/Print;)V
  #41 = MethodHandle       #6:#56         // invokestatic test/LambdaTest.Lambda$main$1:(Ljava/lang/String;)V
  #42 = NameAndType        #20:#57        // print:()Ltest/Print;
  #43 = Class              #58            // java/lang/System
  #44 = NameAndType        #59:#60        // out:Ljava/io/PrintStream;
  #45 = Class              #61            // java/io/PrintStream
  #46 = NameAndType        #62:#25        // println:(Ljava/lang/String;)V
  #47 = Utf8               java/lang/StringBuilder
  #48 = NameAndType        #63:#64        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #49 = NameAndType        #65:#66        // toString:()Ljava/lang/String;
  #50 = Utf8               test/LambdaTest
  #51 = Utf8               java/lang/Object
  #52 = Utf8               test/Print
  #53 = Methodref          #67.#68        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHan
dles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;L
java/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #54 = Methodref          #14.#69        // test/LambdaTest.Lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)V
  #55 = Utf8               (Ljava/lang/String;)Ltest/Print;
  #56 = Methodref          #14.#70        // test/LambdaTest.Lambda$main$1:(Ljava/lang/String;)V
  #57 = Utf8               ()Ltest/Print;
  #58 = Utf8               java/lang/System
  #59 = Utf8               out
  #60 = Utf8               Ljava/io/PrintStream;
  #61 = Utf8               java/io/PrintStream
  #62 = Utf8               println
  #63 = Utf8               append
  #64 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #65 = Utf8               toString
  #66 = Utf8               ()Ljava/lang/String;
  #67 = Class              #71            // java/lang/invoke/LambdaMetafactory
  #68 = NameAndType        #72:#76        // 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;
  #69 = NameAndType        #26:#27        // Lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)V
  #70 = NameAndType        #24:#25        // Lambda$main$1:(Ljava/lang/String;)V
  #71 = Utf8               java/lang/invoke/LambdaMetafactory
  #72 = Utf8               metafactory
  #73 = Class              #78            // java/lang/invoke/MethodHandles$Lookup
  #74 = Utf8               Lookup
  #75 = Utf8               InnerClasses
  #76 = 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;
  #77 = Class              #79            // java/lang/invoke/MethodHandles
  #78 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #79 = Utf8               java/lang/invoke/MethodHandles
{
  public test.LambdaTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0

  public static void print(java.lang.String, test.Print);
    descriptor: (Ljava/lang/String;Ltest/Print;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_1
         1: aload_0
         2: invokeinterface #2,  2            // InterfaceMethod test/Print.print:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 9: 0
        line 10: 7

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #3                  // String Chen Longfei
         2: astore_1
         3: ldc           #4                  // String hello,
         5: astore_2
         6: aload_1
         7: aload_2
         8: invokedynamic #5,  0              // InvokeDynamic #0:print:(Ljava/lang/String;)Ltest/Print;
        13: invokestatic  #6                  // Method print:(Ljava/lang/String;Ltest/Print;)V
        16: aload_1
        17: invokedynamic #7,  0              // InvokeDynamic #1:print:()Ltest/Print;
        22: invokestatic  #6                  // Method print:(Ljava/lang/String;Ltest/Print;)V
        25: return
      LineNumberTable:
        line 13: 0
        line 14: 3
        line 16: 6
        line 18: 16
        line 19: 25

  private static void Lambda$main$1(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     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 18: 0

  private static void Lambda$main$0(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #10                 // class java/lang/StringBuilder
         6: dup
         7: invokespecial #11                 // Method java/lang/StringBuilder."<init>":()V
        10: aload_0
        11: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: aload_1
        15: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        21: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        24: return
      LineNumberTable:
        line 16: 0
}
SourceFile: "LambdaTest.java"
InnerClasses:
     public static final #74= #73 of #77; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #36 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:
      #37 (Ljava/lang/String;)V
      #38 invokestatic test/LambdaTest.Lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)V
      #37 (Ljava/lang/String;)V
  
  1: #36 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:
      #37 (Ljava/lang/String;)V
      #41 invokestatic test/LambdaTest.Lambda$main$1:(Ljava/lang/String;)V
      #37 (Ljava/lang/String;)V

這個 class 文件展示了三個主要部分:

  • 常量池
  • 構造方法和 main、print、Lambdamain0、Lambdamain1方法
  • Lambda表達式生成的內(nèi)部類。

重點看下main方法的實現(xiàn):

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=3, args_size=1
     // 將字符串常量"Chen Longfei"從常量池壓棧到操作數(shù)棧
     0: ldc           #3                  // String Chen Longfei
     
     // 將棧頂引用型數(shù)值存入第二個本地變,即 String name = "Chen Longfei"
     2: astore_1
     
     // 將字符串常量"hello,"從常量池壓棧到操作數(shù)棧
     3: ldc           #4                  // String hello,
     
     // 將棧頂引用型數(shù)值存入第三個本地變量, 即  String prefix = "hello, "
     5: astore_2
     
     //將第二個引用類型本地變量推送至棧頂,即  name
     6: aload_1
     
     //將第三個引用類型本地變量推送至棧頂,即 prefix
     7: aload_2
     
     //通過invokedynamic指令創(chuàng)建Print接口的實匿名內(nèi)部類,實現(xiàn) (t) -> System.out.println(prefix + t)
     8: invokedynamic #5,  0              // InvokeDynamic #0:print:(Ljava/lang/String;)Ltest/Print;
    
     //調(diào)用靜態(tài)方法print
     13: invokestatic  #6                  // Method print:(Ljava/lang/String;Ltest/Print;)V
    
     //將第二個引用類型本地變量推送至棧頂,即  name
     16: aload_1
     
     //通過invokedynamic指令創(chuàng)建Print接口的匿名內(nèi)部類,實現(xiàn) (t) -> System.out.println(t)
    17: invokedynamic #7,  0              // InvokeDynamic #1:print:()Ltest/Print;
    
     //調(diào)用靜態(tài)方法print
    22: invokestatic  #6                  // Method print:(Ljava/lang/String;Ltest/Print;)V
    25: return
    ……

兩個匿名內(nèi)部類是通過BootstrapMethods方法創(chuàng)建的:

   //匿名內(nèi)部類
    InnerClasses:
        public static final #74= #73 of #77; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
   BootstrapMethods:
     //調(diào)用靜態(tài)工廠LambdaMetafactory.metafactory創(chuàng)建匿名內(nèi)部類1。實現(xiàn)了 (t) -> System.out.println(prefix + t)
     0: #36 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:
         #37 (Ljava/lang/String;)V
       //該類會調(diào)用靜態(tài)方法LambdaTest.Lambda$main$0
         #38 invokestatic test/LambdaTest.Lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)V
         #37 (Ljava/lang/String;)V
     
     //調(diào)用靜態(tài)工廠LambdaMetafactory.metafactory創(chuàng)建匿名內(nèi)部類2,實現(xiàn)了 (t) -> System.out.println(t)
     1: #36 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:
         #37 (Ljava/lang/String;)V
         //該類會調(diào)用靜態(tài)方法LambdaTest.Lambda$main$1
         #41 invokestatic test/LambdaTest.Lambda$main$1:(Ljava/lang/String;)V
         #37 (Ljava/lang/String;)V

可以在運行時加上-Djdk.internal.Lambda.dumpProxyClasses=%PATH%,加上這個參數(shù)后,運行時,會將生成的內(nèi)部類class輸出到%PATH%路徑下。

javap -p -c 反編譯兩個文件:

//print(name, (t) -> System.out.println(t))的實例
final class test.LambdaTest$$Lambda$1 implements test.Print {
      private test.LambdaTest$$Lambda$1(); //構造方法
        Code:
           0: aload_0
           1: invokespecial #10                 // Method java/lang/Object."<init>":()V
           4: return
                   
      //實現(xiàn)test.Print接口方法
      public void print(java.lang.String);
        Code:
           0: aload_1
           //調(diào)用靜態(tài)方法LambdaTest.Lambda$1
           1: invokestatic  #18                 // Method test/LambdaTest.Lambda$1:(Ljava/lang/String;)V
           4: return
    }

    
    //print(name, (t) -> System.out.println(prefix + t))的實例
    final class test.LambdaTest$$Lambda$2 implements test.Print {
      
      private final java.lang.String arg$1;

      private test.LambdaTest$$Lambda$2(java.lang.String);
        Code:
           0: aload_0
           1: invokespecial #13                 // Method java/lang/Object."<init>":()V
           4: aload_0
           5: aload_1
           //final變量arg$1由構造方法傳入
           6: putfield      #15                 // Field arg$1:Ljava/lang/String;
           9: return

      //該方法返回一個 LambdaTest$$Lambda$2實例
      private static test.Print get$Lambda(java.lang.String);
        Code:
           0: new           #2                  // class test/LambdaTest$$Lambda$2
           3: dup
           4: aload_0
           5: invokespecial #19                 // Method "<init>":(Ljava/lang/String;)V
           8: areturn
           
      //實現(xiàn)test.Print接口方法
      public void print(java.lang.String);
        Code:
           0: aload_0
           1: getfield      #15                 // Field arg$1:Ljava/lang/String;
           4: aload_1
           
           //調(diào)用靜態(tài)方法LambdaTest.Lambda$0
           5: invokestatic  #27                 // Method test/LambdaTest.Lambda$0:(Ljava/lang/String;Ljava/lang/String;)V
           8: return
    }

對比兩個實例,可以發(fā)現(xiàn),由于表達式print(name, (t) -> System.out.println(prefix + t))引用了局部變量prefixLambdaTest$$Lambda$2類多了一個final參數(shù):

private final java.lang.String arg$1

該參數(shù)由構造方法傳入,用來存儲main方法中的局部變量prefix

String prefix = "hello, ";

由于外部類的main方法與匿名內(nèi)部類LambdaTest$$Lambda$2引用了同一份變量,該變量雖然在代碼層面獨立存儲于兩個類當中,但是在邏輯上具有一致性,所以匿名內(nèi)部類中加上了final關鍵字,而外部類中雖然沒有為prefix顯式地添加final,但是在被Lambda表達式引用后,該變量就自動隱含了final語意(再次更改會報錯)。

2、InvokeDynamic

通過上面的例子可以發(fā)現(xiàn),Lambda表達式由虛擬機指令InvokeDynamic實現(xiàn)方法調(diào)用。

2.1 方法調(diào)用

方法調(diào)用不等同于方法執(zhí)行,方法調(diào)用階段的唯一任務就是確定被調(diào)用方法的版本(即確定具體調(diào)用那一個方法),不涉及方法內(nèi)部具體運行。

java虛擬機中提供了5條方法調(diào)用的字節(jié)碼指令:

  • invokestatic:調(diào)用靜態(tài)方法
  • invokespecial:調(diào)用實例構造器<init>方法、私有方法、父類方法
  • invokevirtual:調(diào)用虛方法。
  • invokeinterface:調(diào)用接口方法,在運行時再確定一個實現(xiàn)該接口的對象
  • invokedynamic:運行時動態(tài)解析出調(diào)用的方法,然后去執(zhí)行該方法。

invokeDynamic是 java 7 引入的一條新的虛擬機指令,這是自 1.0 以來第一次引入新的虛擬機指令。到了 java 8 這條指令才第一次在 java 應用,用在 Lambda 表達式中。invokeDynamic與其他invoke指令不同的是它允許由應用級的代碼來決定方法解析。

2.2 指令規(guī)范

根據(jù)JVM規(guī)范的規(guī)定,invokeDynamic的操作碼是186(0xBA),格式是:

invokedynamic indexbyte1 indexbyte2 0 0

invokeDynamic指令有四個操作數(shù),前兩個操作數(shù)構成一個索引[ (indexbyte1 << 8) | indexbyte2 ],指向類的常量池,后兩個操作數(shù)保留,必須是0。

查看上例中LambdaTest類的反編譯結果,第一處Lambda表達式

print(name, (t) -> System.out.println(t));

對應的指令為:

17: invokedynamic #7, 0 // InvokeDynamic #1:print:()Ltest/Print;

常量池中#7對應的常量為:

#7 = InvokeDynamic #1:#42 // #1:print:()Ltest/Print;

其類型為CONSTANT_InvokeDynamic_infoCONSTANT_InvokeDynamic_info結構是Java7新引入class文件的,其用途就是給invokeDynamic指令指定啟動方法(bootstrap method)、調(diào)用點call site()等信息, 實際上是個 MethodHandle(方法句柄)對象。

#1代表BootstrapMethods表中的索引,即

BootstrapMethods:


//第一個
0: #36 …… 

 //第二個
  1: #36 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:

      # 37 (Ljava/lang/String;)V
      # 41 invokestatic test/LambdaTest.Lambda$main$1:(Ljava/lang/String;)V
      # 37 (Ljava/lang/String;)V

也就是說,最終調(diào)用的是java.lang.invoke.LambdaMetafactory類的靜態(tài)方法metafactory()。

2.3 執(zhí)行過程

為了更深入的了解invokeDynamic,先來看幾個術語:

  • dynamic call site
    程序中出現(xiàn)Lambda的地方都被稱作dynamic call site,CallSite 就是一個 MethodHandle(方法句柄)的 holder。方法句柄指向一個調(diào)用點真正執(zhí)行的方法。
  • bootstrap method
    java里對所有Lambda的有統(tǒng)一的bootstrap method(LambdaMetafactory.metafactory),bootstrap運行期動態(tài)生成了匿名類,將其與CallSite綁定,得到了一個獲取匿名類實例的call site object
  • call site object
    call site object持有MethodHandle的引用作為它的target,它是bootstrap method方法成功調(diào)用后的結果,將會與 dynamic call site永久綁定。call site object的target會被JVM執(zhí)行,就如同執(zhí)行一條invokevirtual指令,其所需的參數(shù)也會被壓入operand stack。最后會得一個實現(xiàn)了functional interface的對象。

InvokeDynamic 首先需要生成一個 CallSite(調(diào)用點對象),CallSite 是由 bootstrap method 返回,也就是調(diào)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();
    }

前三個參數(shù)是固定的,由VM自動壓棧:

  • MethodHandles.Lookup caller代表InvokeDynamic 指令所在的類的上下文(在上例中就是LambdaTest),可以通過 Lookup#lookupClass()獲取這個類
  • String invokedName表示要實現(xiàn)的方法名(在上例中就是Print接口的方法名“print”)
  • MethodType invokedType call site object所持有的MethodHandle需要的參數(shù)和返回類型(signature)

接下來就是附加參數(shù),這些參數(shù)是靈活的,由Bootstrap methods表提供:

  • MethodType samMethodType表示要實現(xiàn)functional interface里面抽象方法的類型
  • MethodHandle implMethod表示編譯器給生成的 desugar 方法,是一個 MethodHandle
  • MethodType instantiatedMethodType即運行時的類型,因為方法定義可能是泛型,傳入時可能是具體類型String之類的,要做類型校驗強轉(zhuǎn)等等

LambdaMetafactory.metafactory 方法會創(chuàng)建一個VM Anonymous Class,這個類是通過 ASM 編織字節(jié)碼在內(nèi)存中生成的,然后直接通過 UNSAFE 直接加載而不會寫到文件里。VM Anonymous Class 是真正意義上的匿名類,不需要 ClassLoader 加載,沒有類名,當然也沒其他權限管理等操作,這意味著效率更高(不必要的鎖操作)、GC 更方便(沒有 ClassLoader)。

2.4 MethodHandle

要讓invokedynamic正常運行,一個核心的概念就是方法句柄(method handle)。它代表了一個可以從invokedynamic調(diào)用點進行調(diào)用的方法。每個invokedynamic指令都會與一個特定的方法關聯(lián)(也就是bootstrap method或BSM)。當編譯器遇到invokedynamic指令的時候,BSM會被調(diào)用,會返回一個包含了方法句柄的對象,這個對象表明了調(diào)用點要實際執(zhí)行哪個方法。

Java 7 API中加入了java.lang.invoke.MethodHandle(及其子類),通過它們來代表invokedynamic指向的方法。
一個Java方法可以視為由四個基本內(nèi)容所構成:

  • 名稱
  • 簽名(包含返回類型)
  • 定義它的類
  • 實現(xiàn)方法的字節(jié)碼

這意味著如果要引用某個方法,我們需要有一種有效的方式來表示方法簽名(而不是反射中強制使用的令人討厭的Class<?>[] hack方式)。

方法句柄首先需要的一個表達方法簽名的方式,以便于查找。在Java 7引入的Method Handles API中,這個角色是由java.lang.invoke.MethodType類來完成的,它使用一個不可變的實例來代表簽名。要獲取MethodType,我們可以使用methodType()工廠方法。這是一個參數(shù)可變的方法,以class對象作為參數(shù)。

第一個參數(shù)所使用的class對象,對應著簽名的返回類型;剩余參數(shù)中所使用的class對象,對應著簽名中方法參數(shù)的類型。例如:

//toString()的簽名
MethodType mtToString = MethodType.methodType(String.class);

// setter方法的簽名
MethodType mtSetter = MethodType.methodType(void.class, Object.class);

// Comparator中compare()方法的簽名
MethodType mtStringComparator = MethodType.methodType(int.class, String.class, String.class); 

現(xiàn)在我們就可以使用MethodType,再組合方法名稱以及定義方法的類來查找方法句柄。要實現(xiàn)這一點,我們需要調(diào)用靜態(tài)的MethodHandles.lookup()方法。這樣的話,會給我們一個“查找上下文(lookup context)”,這個上下文基于當前正在執(zhí)行的方法(也就是調(diào)用lookup()的方法)的訪問權限。

查找上下文對象有一些以“find”開頭的方法,例如,findVirtual()、findConstructor()、findStatic()等。這些方法將會返回實際的方法句柄,需要注意的是,只有在創(chuàng)建查找上下文的方法能夠訪問(調(diào)用)被請求方法的情況下,才會返回句柄。這與反射不同,我們沒有辦法繞過訪問控制。換句話說,方法句柄中并沒有與setAccessible()對應的方法。例如

    public MethodHandle getToStringMH() {
        MethodHandle mh = null;
        MethodType mt = MethodType.methodType(String.class);
        MethodHandles.Lookup lk = MethodHandles.lookup();

        try {
            mh = lk.findVirtual(getClass(), "toString", mt);
        } catch (NoSuchMethodException | IllegalAccessException mhx) {
            throw (AssertionError) new AssertionError().initCause(mhx);
        }

        return mh;
    }

MethodHandle中有兩個方法能夠觸發(fā)對方法句柄的調(diào)用,那就是invoke()invokeExact()。這兩個方法都是以接收者(receiver)和調(diào)用變量作為參數(shù),所以它們的簽名為:

public final Object invoke(Object... args) throws Throwable;
public final Object invokeExact(Object... args) throws Throwable; 

兩者的區(qū)別在于,invokeExact()在調(diào)用方法句柄時會試圖嚴格地直接匹配所提供的變量。而invoke()與之不同,在需要的時候,invoke()能夠稍微調(diào)整一下方法的變量。invoke()會執(zhí)行一個asType()轉(zhuǎn)換,它會根據(jù)如下的這組規(guī)則來進行變量的轉(zhuǎn)換:

  • 如果需要的話,原始類型會進行裝箱操作

  • 如果需要的話,裝箱后的原始類型會進行拆箱操作

  • 如果必要的話,原始類型會進行擴展

  • void返回類型會轉(zhuǎn)換為0(對于返回原始類型的情況),而對于預期得到引用類型的返回值的地方,將會轉(zhuǎn)換為null

  • null值會被視為正確的,不管靜態(tài)類型是什么都可以進行傳遞

接下來,我們看一下考慮上述規(guī)則的簡單調(diào)用樣例:

    Object rcvr = "a";
    try {
        MethodType mt = MethodType.methodType(int.class);
        MethodHandles.Lookup l = MethodHandles.lookup();
        MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt);

        int ret;
        try {
            ret = (int) mh.invoke(rcvr);
            System.out.println(ret);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    } catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) {
        e.printStackTrace();
    } catch (IllegalAccessException x) {
        x.printStackTrace();
    }

上面的代碼調(diào)用了Objecthashcode()方法,看到這里,你肯定會說這不就是 Java 的反射嗎?

確實,MethodHandl和 Reflection實現(xiàn)的功能有太多相似的地方,都是運行時解析方法調(diào)用,理解方法句柄的一種方式就是將其視為以安全、現(xiàn)代的方式來實現(xiàn)反射的核心功能,在這個過程會盡可能地保證類型的安全。

但是,究其本質(zhì),兩者之間還是有區(qū)別的:

Reflection中的java.lang.reflect.Method對象遠比MethodHandl機制中的java.lang.invoke.MethodHandle`對象所包含的信息來得多。前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式,還包含有執(zhí)行權限等的運行期信息。而后者僅僅包含著與執(zhí)行該方法相關的信息。用開發(fā)人員通俗的話來講,Reflection是重量級,而MethodHandle是輕量級

  • 從性能角度上說,MethodHandle 要比反射快很多,因為訪問檢查在創(chuàng)建的時候就已經(jīng)完成了,而不是像反射一樣等到運行時候才檢查
  • Reflection是在模擬Java代碼層次的方法調(diào)用,而MethodHandle是在模擬字節(jié)碼層次的方法調(diào)用。
  • MethodHandle 是結合 invokedynamic 指令一起為動態(tài)語言服務的,也就是說MethodHandle (更準確的來說是其設計理念)是服務于所有運行在JVM之上的語言,而 Relection 則只是適用 Java 語言本身。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Invokedynamic指令是java7中加入的字節(jié)碼指令,理解這條指令可以讓我們熟悉程序的執(zhí)行流程,這篇文章將...
    請輸入妮稱閱讀 3,362評論 0 1
  • inDy(invokedynamic)是 java 7 引入的一條新的虛擬機指令,這是自 1.0 以來第一次引入新...
    TiouLims閱讀 18,960評論 3 31
  • 虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存, 并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化, 最終形成可以被虛擬機直接使...
    好好學習Sun閱讀 1,380評論 0 3
  • 有一首歌這樣唱著 夢和信仰,尚有微光 詩和遠方,亦有暖陽 默默叨念著 不是虛無的夢想 而是努力的方向 走在現(xiàn)實之路...
    淺暖姑娘閱讀 355評論 1 2
  • 1.概念: 操作系統(tǒng)(Operating System,簡稱OS)是管理和控制計算機硬件與軟件資源的計算機程序,是...
    濃睡不消殘醉閱讀 1,321評論 0 5

友情鏈接更多精彩內(nèi)容