本篇將從編譯,執(zhí)行層面為大家講解函數(shù)式接口運行的機制,讓各位小伙伴更進一步加深對函數(shù)式接口的理解
概述
函數(shù)式接口包含三部分內(nèi)容:
- (應用篇一JDK源碼解析——深入函數(shù)式接口(應用篇一))(1)函數(shù)式接口的來源,(2)Lambda表達式,(3)雙冒號運算符
- (應用篇二函數(shù)式編程,這樣學就廢了)(4)詳細介紹@FunctionInterface注解(5)對java.util.function包進行解讀
- (原理篇)介紹函數(shù)式接口的實現(xiàn)原理 在看本篇之前,請大家對應先看應用篇一和應用篇二,本篇作為原理篇,將為大家較為深入的剖析函數(shù)式接口如何編譯,JVM又是如何關(guān)聯(lián)銜接各個部分的。
說明:源碼使用的版本為JDK-11.0.11
編譯
首先我們從編譯出發(fā),因為無論是接口還是類,都需要經(jīng)過編譯,然后在運行期由JVM執(zhí)行調(diào)用,現(xiàn)在我們來看看幾個關(guān)鍵位置的編譯結(jié)果。先來看函數(shù)式接口編譯
Classfile /O:/SCM/ws-java/sample-lambda/bin/com/tree/sample/func/IFuncInterfaceSample.class
Last modified 2021-6-4; size 238 bytes
MD5 checksum 58a3c8c5cbe9c7498e86d4a349554ae0
Compiled from "IFuncInterfaceSample.java"
public interface com.tree.sample.func.IFuncInterfaceSample
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #2 // com/tree/sample/func/IFuncInterfaceSample
#2 = Utf8 com/tree/sample/func/IFuncInterfaceSample
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 func1
#6 = Utf8 ()V
#7 = Utf8 SourceFile
#8 = Utf8 IFuncInterfaceSample.java
#9 = Utf8 RuntimeVisibleAnnotations
#10 = Utf8 Ljava/lang/FunctionalInterface;
{
public abstract void func1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "IFuncInterfaceSample.java"
RuntimeVisibleAnnotations:
0: #10()
接口的編譯信息中沒有任何額外的工作,如果顯示聲明了FunctionInterface注解,則編譯信息中帶有,反之則無。
接下來,我們著重來看應用部分的代碼編譯的情況,先看應用部分的源代碼:
public class LambdaBinaryCode {
private int lambdaVar = 100;
public static void main(String[] args) {
LambdaBinaryCode ins = new LambdaBinaryCode();
ins.invokeLambda();
ins.invokeEta();
ins.invokeLambda2();
}
/**
* 簡單的函數(shù)式編程示例
*/
public void invokeLambda() {
// 準備測試數(shù)據(jù)
Integer[] data = new Integer[] {1, 2, 3};
List< Integer > list = Arrays.asList(data);
// 簡單示例:打印List數(shù)據(jù)
list.forEach(x - > System.out.println(String.format("Cents into Yuan: %.2f", x/100.0)));
}
/**
* 簡單的函數(shù)式編程示例
*/
public void invokeEta() {
// 準備測試數(shù)據(jù)
Integer[] data = new Integer[] {1, 2, 3};
List< Integer > list = Arrays.asList(data);
// 通過eta操作符訪問
list.forEach(System.out::println);
}
/**
* 簡單的函數(shù)式編程示例
*/
public void invokeLambda2() {
// 準備測試數(shù)據(jù)
Map< Integer, Integer > map = new HashMap< Integer, Integer >();
int count = 10;
Random r = new Random();
while(count-- >0) {
map.put(r.nextInt(100), r.nextInt(10000));
}
// Lambda調(diào)用示例
map.forEach((x, y) - > {
System.out.println(String.format("Map key: %1s, value: %2s", x, y+lambdaVar));
});
}
}
這段源碼中選取了幾種典型的場景進行組合,讓大家了解更多的擴展知識,因此代碼稍顯長。
- invokeLambda() 單個參數(shù)的lambda表達式,省略參數(shù)括號和表達式主體的花括號。
- invokeEta() eta方式的方法引用。
- invokeLambda2() 兩個參數(shù)的lambda表達式,lambda中使用成員變量。
lambda表達式的編譯
指北君和大家一起看看編譯后的內(nèi)容,使用命令查看編譯后的方法結(jié)構(gòu)(javap -p com.tree.sample.func.LambdaBinaryCode)
Compiled from "LambdaBinaryCode.java"
public class com.tree.sample.func.LambdaBinaryCode {
private int lambdaVar;
public com.tree.sample.func.LambdaBinaryCode();
public static void main(java.lang.String[]);
public void invokeLambda();
public void invokeEta();
public void invokeLambda2();
private static void lambda$0(java.lang.Integer);
private void lambda$2(java.lang.Integer, java.lang.Integer);
}
小伙伴有沒發(fā)現(xiàn),class文件中比源碼文件中多出了兩個方法:lambda2。這兩個方法分別對應invokeLambda和invokeLambda2中的的lambda表達式。
我們在javap命令中增加-v參數(shù),可以查看到增加的 方法的更多細節(jié),不熟悉JVM指令的小伙伴也不用擔心,我們只是驗證 就是invokeLambda中l(wèi)ambda表達式對應“x -> System.out.println(String.format("Cents into Yuan: %.2f", x/100.0))”。
private static void lambda$0(java.lang.Integer);
descriptor: (Ljava/lang/Integer;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=9, locals=1, args_size=1
0: getstatic #61 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #105 // String Cents into Yuan: %.2f
5: iconst_1
6: anewarray #3 // class java/lang/Object
9: dup
10: iconst_0
11: aload_0
12: invokevirtual #107 // Method java/lang/Integer.intValue:()I
15: i2d
16: ldc2_w #111 // double 100.0d
19: ddiv
20: invokestatic #113 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
23: aastore
24: invokestatic #118 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
27: invokevirtual #124 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 30: 0
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 x Ljava/lang/Integer;
從編譯信息中我們可以看到幾條明顯相同的邏輯:
- LocalVariableTable 首先包含了函數(shù)的輸入?yún)?shù),并且一致
- 24行執(zhí)行String.format方法
- 27行執(zhí)行PrintStream.println方法 從上面三個關(guān)鍵部分我們可以確定就是invokeLambda方法中的lambda表達式編譯后的內(nèi)容了。
仔細的小伙伴比較 和 兩個方法后,可能會發(fā)現(xiàn)兩個問題:
- 兩個方法怎么一個是static一個是非static的呢?
- 方法命名中的數(shù)字為什么不是數(shù)字連續(xù)的?
對于第一個問題,比較invokeLambda和invokeLambda2的源碼,小伙伴發(fā)現(xiàn)有什么不同么?是否可以看到invokeLambda2中的lambda表達式引用了成員屬性lambdaVar。這就是lambda生成方法的一種邏輯, 未使用成員變量的lambda表達式編譯成靜態(tài)方法,使用了成員變量的lambda語句則編譯為成員方法 。
第二個問題我們將留待后面回答。
Lambda調(diào)用
上面我們看到了lambda表達式的代碼編譯成了一個獨立方法,指北君繼續(xù)帶領(lǐng)大家查看編譯后的文件,我們要了解編譯后lambda方法是如何調(diào)用執(zhí)行的。查看invokeLambda方法的編譯后的內(nèi)容(直貼出了關(guān)鍵部分):
public void invokeLambda();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=1
... ...
32: istore_3
33: aload_2
34: invokedynamic #45, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
39: invokeinterface #49, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
... ...
在invokeLambda中有一個指令invokedynamic,熟悉動態(tài)語言的小伙伴可能知道,這個指令是Java7為支持動態(tài)腳本語言而增加的。而函數(shù)式Java調(diào)用函數(shù)接口也正是通過invokedynamic指令來實現(xiàn)的。invokeLambda的詳細內(nèi)容指北君后續(xù)單獨為大家講解,今天我們關(guān)注函數(shù)接口的調(diào)用過程。
使用invokeLambda指令,那么該指令是直接調(diào)用的lambda$0方法么?我們知道list.forEach(xx)調(diào)用中,我們是將函數(shù)接口作為參數(shù)傳遞到其他類的函數(shù)中進行執(zhí)行的。Java需要解決兩個問題:
1)如何將方法傳遞給被調(diào)用的外部類的方法。
2)外部的類和方法如何訪問我們內(nèi)部私有的方法。
引導方法表
為解決上面兩個問題,我們繼續(xù)查編譯后的文件,在末尾,我們看到下面的部分:
BootstrapMethods:
0: #146 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:
#148 (Ljava/lang/Object;)V
#151 invokestatic com/tree/sample/func/LambdaBinaryCode.lambda$0:(Ljava/lang/Integer;)V
#152 (Ljava/lang/Integer;)V
1: #146 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:
#153 (Ljava/lang/Object;)V
#156 invokevirtual java/io/PrintStream.println:(Ljava/lang/Object;)V
#157 (Ljava/lang/Integer;)V
2: #146 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:
#159 (Ljava/lang/Object;Ljava/lang/Object;)V
#162 invokespecial com/tree/sample/func/LambdaBinaryCode.lambda$2:(Ljava/lang/Integer;Ljava/lang/Integer;)V
#163 (Ljava/lang/Integer;Ljava/lang/Integer;)V
InnerClasses:
public static final #169= #165 of #167; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
這生成了三個引導方法,剛好和我們的三個函數(shù)接口調(diào)用一致,從引導方法的參數(shù)我們看出
序號 | 調(diào)用 | 調(diào)用類型 |
---|---|---|
0 | lambda$0 | static |
1 | PrintStream.println | vertual |
2 | lambda$2 | special |
順便回答一下之前的方法名稱的數(shù)字序號不連續(xù)問題,我們看出,方法名稱的序號是根據(jù)引導方法的序號來確定的,不是根據(jù)生成的lambda表達式方法序號來的。我們看到,引導方法的邏輯似乎就是調(diào)用lambda方法或者其他的函數(shù)接口,每個引導方法中都出現(xiàn)了LambdaMetafactory.metafactory方法
動態(tài)調(diào)用
現(xiàn)在,我們結(jié)合invokedynamic指令來說明BootstrapMethods執(zhí)行的過程
動態(tài)調(diào)用邏輯
上面的的流程顯示了動態(tài)調(diào)用的基本邏輯
- 執(zhí)行invokedynamic
- 檢查調(diào)用點是否已連接可用
- 如果未連接,構(gòu)建動態(tài)調(diào)用點
- 執(zhí)行引導方法
- 生成并加載調(diào)用點對應的動態(tài)內(nèi)部類
- 連接
- 調(diào)用動態(tài)內(nèi)部類方法
- 內(nèi)部類調(diào)用lambda對應的方法并執(zhí)行
這兩個階段我們通過調(diào)用堆棧也能明顯觀察到:
引導階段
執(zhí)行階段
我們還可以通過設置VM參數(shù)-Djdk.internal.lambda.dumpProxyClasses,查看以引導階段動態(tài)生成的內(nèi)部類:
動態(tài)內(nèi)部類列表
打開其中一個如下:
動態(tài)內(nèi)部類詳情
小結(jié)
動態(tài)函數(shù)接口的調(diào)用原理,給大家介紹到這里了,相信大家看完本篇內(nèi)容后,對函數(shù)式接口有了更深一層的學習。由于涉及的內(nèi)容較多,沒有時間給大家逐一詳細的給每個涉及到的類進行解讀。后續(xù)指北君會根據(jù)小伙伴們需要對今天提及的知識點做深入的階段,比如invokeddynamic指令,class結(jié)構(gòu),動態(tài)調(diào)用相關(guān)的各部分代碼邏輯。
-
接口
+關(guān)注
關(guān)注
33文章
8720瀏覽量
152037 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4349瀏覽量
63027 -
編譯
+關(guān)注
關(guān)注
0文章
663瀏覽量
33074 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12273
發(fā)布評論請先 登錄
相關(guān)推薦
如何查看及更改函數(shù)/函數(shù)塊的調(diào)用環(huán)境
![如何查看及更改<b class='flag-5'>函數(shù)</b>/<b class='flag-5'>函數(shù)</b>塊的<b class='flag-5'>調(diào)用</b>環(huán)境](https://file1.elecfans.com/web2/M00/AE/C0/wKgaomVWvV2ANCozAAAzJenX8j8177.png)
Linux系統(tǒng)動態(tài)庫與靜態(tài)庫函數(shù)的使用介紹
Linux下靜態(tài)庫和動態(tài)庫的制作與使用
C++教程之函數(shù)的遞歸調(diào)用
動態(tài)Feign的“萬能”接口調(diào)用
嵌入式軟件架構(gòu)設計之函數(shù)調(diào)用
![嵌入式軟件架構(gòu)設計之<b class='flag-5'>函數(shù)</b><b class='flag-5'>調(diào)用</b>](https://file.elecfans.com//web2/M00/90/FA/poYBAGPsgDyAPpd2AACMnI3v9PI987.jpg)
什么是函數(shù)的調(diào)用?
SCL中調(diào)用函數(shù)的示例
觸發(fā)器的輸出是現(xiàn)態(tài)函數(shù)
Vivado ML版中動態(tài)函數(shù)交換的技術(shù)進步
![Vivado ML版中<b class='flag-5'>動態(tài)函數(shù)</b>交換的技術(shù)進步](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
隔離設計流程+動態(tài)函數(shù)交換示例
![隔離設計流程+<b class='flag-5'>動態(tài)函數(shù)</b>交換示例](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
使用抽象外殼進行動態(tài)函數(shù)交換的解決方案效率
![使用抽象外殼進行<b class='flag-5'>動態(tài)函數(shù)</b>交換的解決方案效率](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
評論