首頁  >  文章  >  Java  >  Java技巧總結如何看Lambda源碼

Java技巧總結如何看Lambda源碼

WBOY
WBOY轉載
2022-04-18 18:13:501532瀏覽

本篇文章為大家帶來了關於java的相關知識,其中主要介紹了關於如何看Lambda源碼的相關問題,使用Lambda 表達式可以對程式碼進行大量的優化,用幾行程式碼就可以做很多事情,下面一起來看一下,希望對大家有幫助。

Java技巧總結如何看Lambda源碼

推薦學習:《java影片教學

大家都知道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 中的每個元素叫做cp

info,cpinfo 由唯一識別( tag ) 名稱組成,目前tag 的型別一共有:

Java技巧總結如何看Lambda源碼##貼出我們解析出來的部分圖:

    圖中Constant pool 字樣代表目前資訊是常數池;
  1. 每行都是一個 
  2. cp_info

     ,第一列的#1 代表是在常數池下標示為1 的位置;

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

  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 一下,如下圖:

Java技巧總結如何看Lambda源碼

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

以上內容總結一下:

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

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

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

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

Java技巧總結如何看Lambda源碼

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

4、總結

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

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

推薦學習:《java影片教學

以上是Java技巧總結如何看Lambda源碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除