本篇文章為大家帶來了關於java的相關知識,其中主要介紹了關於如何看Lambda源碼的相關問題,使用Lambda 表達式可以對程式碼進行大量的優化,用幾行程式碼就可以做很多事情,下面一起來看一下,希望對大家有幫助。
推薦學習:《java影片教學》
大家都知道Java8 中新增了Lambda 表達式,使用Lambda 表達式可以對程式碼進行大量的最佳化,用幾行程式碼就可以做很多事情,本章以Lambda 為例,第一小節說明一下其底層的執行原理,第二小節說明一下Lambda 流在工作中常用的姿勢。
首先我們來看一個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 代表是在常數池下標示為1 的位置;
每行的第二列,是 cp_info
的唯一識別( tag ) ,例如Methodref 對應上表中的CONSTANT_Methodref(上圖表格中value 對應10的tag),代表當前行是表示方法的描述資訊的,比如說方法的名稱,入參類型,出參數類型等,具體的含義在Java 虛擬機規範中都可以查詢到,Methodref 的截圖如下:
每行的第三列,如果是具體的值的話,直接顯示具體的值,如果是複雜的值的話,會顯示 cp_info
的引用,比如說圖中標紅2 處,引用兩個13 和14 位置的 cp_info
,13 表示方法名字是init,14 表示方法無回傳值,結合起來表示方法的名稱而回傳類型,就是一個無參構造子;
每行的第四列,就是具體的值了。
對於比較重要的cp_info 類型我們說明下其意義:
我們從上圖標示紅色的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 指令,正是因為這個指令,才可以做到雖然編譯時不知道要幹啥,但動態運行時卻能找到具體要執行的代碼。
接著我們看一下在彙編指令輸出的最後,我們發現了異常判斷法中發現的內部類,如下圖:
上圖中箭頭很多,一層一層的表達清楚了目前內部類別的所有資訊。
我們總結一下,Lambda 表達式執行主要是依靠invokedynamic 的JVM 指令來實現,咱們演示的類別的全路徑為:demo.eight.Lambda 感興趣的同學可以自己試試看。
不囉嗦,文章結束,期待三連!
推薦學習:《java影片教學》
以上是Java技巧總結如何看Lambda源碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!