1. 程式人生 > >Java 8 動態型別語言Lambda表示式實現原理分析

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裡已經編譯生成了一個靜態的方法,內部類只需要呼叫該靜態方法


相關推薦

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

Java 8支援動態語言,看到了很酷的Lambda表示式,對一直以靜態型別語言自居的Java,讓人看到了Java虛擬機器可以支援動態語言的目標。 import java.util.function.Consumer; public class Lambda { pub

Java Lambda表示式 實現原理分析

本文分析基於JDK 9 一、目標 本文主要解決兩個問題: 1、函式式介面 到底是什麼? 2、Lambda表示式是怎麼實現的? 先介紹一個jdk的bin目錄下的一個位元組碼檢視工具及反編譯工具:javap 二、函式式介面 @Funct

Java 8新特性—02.Lambda 表示式基礎語法

Lambda 表示式的基礎語法:Java8中引入了一個新的操作符“->” 該操作符稱為箭頭操作符或Lambda操作符, 該操作符將Lambda表示式拆分為兩部分: 左側:Lambda 表示式的引數部分 右側:Lamdba 表示式中所需執行的功能,即Lambda 體。

Java 8 學習筆記3——Lambda 表示式

Lambda 表示式簡介 利用行為引數化來傳遞程式碼有助於應對不斷變化的需求。它允許你定義一個程式碼塊來表示一個行為,然後傳遞它。你可以決定在某一事件發生時(例如單擊一個按鈕)或在演算法中的某個特定時刻(例如篩選演算法中類似於“重量超過150克的蘋果”的謂詞,或排序中的自定義比較操作)執

Java 8 新特性:Lambda 表示式之方法引用(Lambda 表示式補充版)

方法引用 文 | 莫若吻      (注:此文乃個人查詢資料然後學習總結的,若有不對的地方,請大家指出,非常感謝!) 1.方法引用簡述 方法引用是用來直接訪問類或者例項的已經存在的方法或

最全最強 Java 8 - 函式程式設計(lambda表示式

Java 8 - 函式程式設計(lambda表示式) 我們關心的是如何寫出好程式碼,而不是符合函式程式設計風格的程式碼。 @pdai Java 8 - 函式程式設計(lambda表示式) 簡介 lambda表示式 分類 惰性求值方法 及早求值方法 stream & parallelStre

無情的Java 8 之 Stream和lambda表示式

  不好意思,最近刷小視訊刷的有點上頭 看到這圖就不自覺的要來一句:"臥槽,無情" 好了,我要開始正經了 JAVA 8 已經推出有一段時間了, 相比之前, 我們操作集合的方式應該是這樣?     這樣?   或者是這樣?

Java 8 Lambda表示式實現設計模式:命令模式

在這篇部落格裡,我將說明如何在使用 Java 8 Lambda表示式 的函數語言程式設計方式 時實現 命令 設計模式 。命令模式的目標是將請求封裝成一個物件,從對客戶端的不同型別請求,例如佇列或日誌請求引數化,並提供相應的操作。命令模式是一種通用程式設計方式,該方式基於執行

Java 8 函數語言程式設計 Lambda

Lambda表示式 a function (or a subroutine) defined, and possibly called, without being bound to an identifier。 一段帶有輸入引數的可執行語句塊。 在Java 8

【深入Java虛擬機器】之八動態型別語言支援

編譯型語言和解釋型語言 1、編譯型語言 需通過編譯器(compiler)將原始碼編譯成機器碼,之後才能執行的語言。一般需經過編譯(compile)、連結(linker)這兩個步驟。 編譯是把原始碼編譯成機器碼, 連結是把各個模組的機器碼和依賴庫串連起來生成可執行檔案。 優

採用java8 lambda表示式 實現 java list 交集 並集 差集 去重複並集

採用java8 lambda表示式 實現java list 交集/並集/差集/去重並集 一般的javaList 交、並集採用簡單的 removeAll retainAll 等操作,不過這也破壞了原始的javaList物件,採用java8 lambda表示式流操

Java JDK 動態代理使用及實現原理分析

一、什麼是代理? 代理是一種常用的設計模式,其目的就是為其他物件提供一個代理以控制對某個物件的訪問。代理類負責為委託類預處理訊息,過濾訊息並轉發訊息,以及進行訊息被委託類執行後的後續處理。 代理模式 UML 圖: 簡單結構示意圖: 為了保持行為的一致性,代

Python:lambda表示式實現求兩個變數的最大值

lambda 表示式(又稱匿名函式)   作用:     建立一個匿名函式物件     同def 類似,但不提供函式名   格式:     lambda [引數1,引數2,.....]: 表示式(預設只能寫一個)   說明:     1.lambda 只是一個表示式,它用

linq中order by 和group by (含lambda表示式實現)以及綜合案例

一、Linq對誰適用 linq的語法通過System.Linq下面的Enumerable類提供支援,也就是說,只要是實現了IEnumerable<T>的物件都可以使用Linq的語法來查詢。LINQ定義了大約40個查詢操作符,如select、from、in、where、group by 以及ord

sql語句-linq語言-lambda表示式對照

1、 查詢Student表中的所有記錄的Sname、Ssex和Class列。 select sname,ssex,class from student Linq:     from s in Students     select new {         s.SNAME,

Java 8 函數語言程式設計探祕 ( 上 )

引子 將行為作為資料傳遞 怎樣在一行程式碼裡同時計算一個列表的和、最大值、最小值、平均值、元素個數、奇偶分組、指數、排序呢? 答案是思維反轉!將行為作為資料傳遞。 文藝青年的程式碼如下所示: public class FunctionUtil {    publ

編譯型語言、解釋型語言、靜態型別語言動態型別語言概念與區別

最近在研究Python和Erlang。反覆提到動態型別語言、動態語言、解釋型語言這些概念。這些概念很生澀,在這裡做一個總結。 編譯型語言和解釋型語言 1、編譯型語言 需通過編譯器(compiler)將原始碼編譯成機器碼,之後才能執行的語言。一般需經過編譯(compile)、連結(linker)這兩個

Java匿名內部類與Lambda表示式

匿名內部類適合建立那種只需要一次使用的類,而且必須繼承一個父類或者實現一個介面,先看一段程式,該程式功能為實現陣列資料的處理。 定義一個命令模式的介面,然後在處理陣列資料的ProcessArray的類

Java 8 函數語言程式設計 如何優雅的使用Optional

Optional Optional是Java8提供的為了解決null安全問題的一個API。 1. Optional 應該只用於返回型別 而不是引數和屬性,不然會使程式碼變的繁瑣,影響可讀性 2 你不應該簡單的呼叫 get() Optinal的目的是為了表示此值有

關於Java是強型別語言的相關解釋

型別系統的一些概念,眾說紛紜,使用上也比較亂。有些東西,甚至不好嚴格定義。以下算學術界的一種相對“嚴格”的說法。 1. 先定義一些基礎概念Program Errors trapped errors。導致程式終止執行,如除0,Java中陣列越界訪問untrapped errors。 出錯後繼續執行,但可能出現