Java技巧總結之如何看Lambda原始碼

2022-04-18 19:00:29
本篇文章給大家帶來了關於的相關知識,其中主要介紹了關於如何看Lambda原始碼的相關問題,使用 Lambda 表示式可以對程式碼進行大量的優化,用幾行程式碼就可以做很多事情,下面一起來看一下,希望對大家有幫助。

推薦學習:《》

大家都知道 Java8 中新增了 Lambda 表示式,使用 Lambda 表示式可以對程式碼進行大量的優化,用幾行程式碼就可以做很多事情,本章以 Lambda 為例,第一小節說明一下其底層的執行原理,第二小節說明一下 Lambda 流在工作中常用的姿勢。

1、Demo

首先我們來看一個 Lambda 表示式的 Demo,如下圖:

程式碼比較簡單,就是新起一個執行緒列印一句話,但對於圖中 () -> System.out.println ( 「 lambda is run 「 ) 這種程式碼,估計很多同學都感覺到很困惑,Java 是怎麼識別這種程式碼的?

如果我們修改成匿名內部類的寫法,就很清楚,大家都能看懂,如下圖:

那是不是說 () -> System.out.println ( 「 lambda is run 「 ) 這種形式的程式碼,其實就是建立了內部類呢?其實這就是最簡單 Lambda 表示式,我們是無法通過 IDEA 看到原始碼和其底層結構的,下面我們就來介紹幾種可看到其底層實現的方式。

2、異常判斷法

我們可以在程式碼執行中主動丟擲異常,列印出堆疊,堆疊會說明其執行軌跡,一般這種方法簡單高效,基本上可以看到很多情況下的隱藏程式碼,我們來試一下,如下圖:

從異常的堆疊中,我們可以看到 JVM 自動給當前類建立了內部類(錯誤堆疊中出現多次的 $ 表示有內部類),內部類的程式碼在執行過程中,丟擲了異常,但這裡顯示的程式碼是 Unknown Source,所以我們也無法 debug 進去,一般情況下,異常都能暴露出程式碼執行的路徑,我們可以打好斷點後再次執行,但對於 Lambda 表示式而言,通過異常判斷法我們只清楚有內部類,但無法看到內部類中的原始碼。

3、javap 命令法

javap 是 Java 自帶的可以檢視 class 位元組碼檔案的工具,安裝過 Java 基礎環境的電腦都可以直接執行 javap 命令,如下圖:

命令選項中,我們主要是用-v -verbose 這個命令,可以完整輸出位元組碼檔案的內容。

接下來我們使用 javap 命令檢視下 Lambda.class 檔案,在講解的過程中,我們會帶上一些關於 class 檔案的知識。

我們在命令視窗中找到 Lambda.class 所在的位置,執行命令:javap -verbose Lambda.class,然後你會看到一長串的東西,這些叫做組合指令,接下來我們來一一講解下( 所有的參考資料來自 Java 虛擬機器器規範,不再一一參照說明):

組合指令中我們很容易找到 Constant pool 打頭的一長串型別,我們叫做常數池,官方英文叫做 Run-Time Constant Pool,我們簡單理解成一個裝滿常數的 table ,table 中包含編譯時明確的數位和文字,類、方法和欄位的型別資訊等等。table 中的每個元素叫做 cpinfo,cpinfo 由唯一標識 ( tag ) + 名稱組成,目前 tag 的型別一共有:

圖片描述

貼出我們解析出來的部分圖:

  1. 圖中 Constant pool 字樣代表當前資訊是常數池;

  2. 每行都是一個 cp_info ,第一列的 #1 代表是在常數池下標為 1 的位置 ;

  3. 每行的第二列,是 cp_info 的唯一標識 ( tag ) ,比如 Methodref 對應著上表中的 CONSTANT_Methodref(上上圖中表格中 value 對應 10 的 tag),代表當前行是表示方法的描述資訊的,比如說方法的名稱,入參型別,出引數型別等,具體的含義在 Java 虛擬機器器規範中都可以查詢到,Methodref 的截圖如下:
    圖片描述

  4. 每行的第三列,如果是具體的值的話,直接顯示具體的值,如果是複雜的值的話,會顯示 cp_info 的參照,比如說圖中標紅 2 處,參照兩個 13 和 14 位置的 cp_info,13 表示方法名字是 init,14 表示方法無返回值,結合起來表示方法的名稱和返回型別,就是一個無參構造器;

  5. 每行的第四列,就是具體的值了。

對於比較重要的 cp_info 型別我們說明下其含義:

  1. InvokeDynamic 表示動態的呼叫方法,後面我們會詳細說明;
  2. Fieldref 表示欄位的描述資訊,如欄位的名稱、型別;
  3. NameAndType 是對欄位和方法型別的描述;
  4. MethodHandle 方法控制程式碼,動態呼叫方法的統稱,在編譯時我們不知道具體是那個方法,但執行時肯定會知道呼叫的是那個方法;
  5. MethodType 動態方法型別,只有在動態執行時才會知道其方法型別是什麼。

我們從上上圖中標紅的 3 處,發現 Ljava/lang/invoke/MethodHandles$Lookup,java/lang/invoke/LambdaMetafactory.metafactory 類似這樣的程式碼,MethodHandles 和 LambdaMetafactory 都是 java.lang.invoke 包下面的重要方法,invoke 包主要實現了動態語言的功能,我們知道 java 語言屬於靜態編譯語言,在編譯的時候,類、方法、欄位等等的型別都已經確定了,而 invoke 實現的是一種動態語言,也就是說編譯的時候並不知道類、方法、欄位是什麼型別,只有到執行的時候才知道。

比如這行程式碼:Runnable runnable = () -> System.out.println(「lambda is run」); 在編譯器編譯的時候 () 這個括號編譯器並不知道是幹什麼的,只有在執行的時候,才會知道原來這代表著的是 Runnable.run() 方法。invoke 包裡面很多類,都是為了代表這些 () 的,我們稱作為方法控制程式碼( MethodHandler ),在編譯的時候,編譯器只知道這裡是個方法控制程式碼,並不知道實際上執行什麼方法,只有在執行的時候才知道,那麼問題來了,JVM 執行的時候,是如何知道 () 這個方法控制程式碼,實際上是執行 Runnable.run() 方法的呢?

首先我們看下 simple 方法的組合指令:

從上圖中就可以看出 simple 方法中的 () -> System.out.println(「lambda is run」) 程式碼中的 (),實際上就是 Runnable.run 方法。

我們追溯到 # 2 常數池,也就是上上圖中標紅 1 處,InvokeDynamic 表示這裡是個動態呼叫,呼叫的是兩個常數池的 cp_info,位置是 #0:#37 ,我們往下找 #37 代表著是 // run:()Ljava/lang/Runnable,這裡表明了在 JVM 真正執行的時候,需要動態呼叫 Runnable.run() 方法,從組合指令上我們可以看出 () 實際上就是 Runnable.run(),下面我們 debug 來證明一下。

我們在上上圖中 3 處發現了 LambdaMetafactory.metafactory 的字樣,通過查詢官方檔案,得知該方法正是執行時, 連結到真正程式碼的關鍵,於是我們在 metafactory 方法中打個斷點 debug 一下,如下圖:

圖片描述

metafactory 方法入參 caller 代表實際發生動態呼叫的位置,invokedName 表示呼叫方法名稱,invokedType 表示呼叫的多個入參和出參,samMethodType 表示具體的實現者的引數,implMethod 表示實際上的實現者,instantiatedMethodType 等同於 implMethod。

以上內容總結一下:

1:從組合指令的 simple 方法中,我們可以看到會執行 Runnable.run 方法;

2:在實際的執行時,JVM 碰到 simple 方法的 invokedynamic 指令,會動態呼叫 LambdaMetafactory.metafactory 方法,執行具體的 Runnable.run 方法。

所以可以把 Lambda 表達值的具體執行歸功於 invokedynamic JVM 指令,正是因為這個指令,才可以做到雖然編譯時不知道要幹啥,但動態執行時卻能找到具體要執行的程式碼。

接著我們看一下在組合指令輸出的最後,我們發現了異常判斷法中發現的內部類,如下圖:

圖片描述

上圖中箭頭很多,一層一層的表達清楚了當前內部類的所有資訊。

4、總結

我們總結一下,Lambda 表示式執行主要是依靠 invokedynamic 的 JVM 指令來實現,咱們演示的類的全路徑為:demo.eight.Lambda 感興趣的同學可以自己嘗試一下。

不囉嗦,文章結束,期待三連!

推薦學習:《》

以上就是Java技巧總結之如何看Lambda原始碼的詳細內容,更多請關注TW511.COM其它相關文章!