Lambda表達(dá)式
簡(jiǎn)介:
Lambda表達(dá)式本質(zhì)上是一種匿名方法锤灿,能將該方法作為參數(shù)傳入其他方法中甜紫,用法上與一個(gè)只需實(shí)現(xiàn)一個(gè)方法的匿名內(nèi)部類很相似揽碘,Lambda表達(dá)式能完全取代這種匿名內(nèi)部類的位置蝉娜,且擁有著更簡(jiǎn)潔的寫(xiě)法和更好的可讀性丙者。
用法:
// ( /*參數(shù)列表*/ )->{ /*具體實(shí)現(xiàn)*/ }
public class MyClass {
String s="OutString";
public static void main(String[] args){
// 比較常用的進(jìn)程的定義
// 匿名內(nèi)部類的寫(xiě)法
new Thread(new Runnable() {
@Override
public void run() {
//具體實(shí)現(xiàn)
s="change";
// 使用外部成員時(shí)花墩,會(huì)隱式地將其指定為final類型悬秉,無(wú)法更改其值,故該部分錯(cuò)誤
}
}).start();
// Lambda表達(dá)式
new Thread(()->{
//具體實(shí)現(xiàn)
}).start();
//需要參數(shù)的情況冰蘑,在此定義一個(gè)Button button=new Button();
button.setOnClickListener((v)->{ //能識(shí)別形參的參數(shù)類型和泌,多個(gè)參數(shù)用逗號(hào)隔開(kāi)
//具體實(shí)現(xiàn)
});
test(()->s); //當(dāng)只有一行代碼時(shí)不需要“{}”,且能自動(dòng)識(shí)別返回類型
}
public static void test(testClass s){}
}
interface testClass{
String m();
}
Lambda表達(dá)式與匿名類的區(qū)別:
如果只是想知道什么場(chǎng)合祠肥、怎么使用Lambda表達(dá)式武氓,上面的部分就足夠了,接下來(lái)將對(duì)Java是如何具體實(shí)現(xiàn)Lambda表達(dá)式和匿名內(nèi)部類做出介紹仇箱,會(huì)發(fā)現(xiàn)县恕,兩者有著本質(zhì)的不同。
靜態(tài)類型語(yǔ)言與動(dòng)態(tài)類型語(yǔ)言
Java是靜態(tài)類型語(yǔ)言剂桥,舉個(gè)栗子:
obj.println("Hello world");
顯然忠烛,這行代碼需要一個(gè)具體的上下文才有討論的意義,假設(shè)它在Java語(yǔ)言中权逗,并且變量obj的類型為java.io.PrintStream美尸,那obj的值就必須是PrintStream的子類(實(shí)現(xiàn)了PrintStream接口的類)才是合法的冤议。否則,哪怕obj擁有一個(gè)println(String)方法师坎,但與PrintStream接口沒(méi)有繼承關(guān)系恕酸,代碼依然不能運(yùn)行——因?yàn)轭愋蜋z查不合法。
但是相同的代碼在動(dòng)態(tài)類型語(yǔ)言ECMAScript(JavaScript)中情況則不一樣屹耐,無(wú)論obj具體是何種類型,只要這種類型的定義中確實(shí)包含有println(String)方法椿猎,那方法調(diào)用便可成功惶岭。
這種差別產(chǎn)生的原因是Java語(yǔ)言在編譯期間卻已將println(String)方法完整的符號(hào)引用(本例中為一項(xiàng)CONSTANT_InterfaceMethodref_info常量)生成出來(lái),作為方法調(diào)用指令的參數(shù)存儲(chǔ)到Class文件中犯眠,例如下面這個(gè)例子:
public void print(){
PrintStream printStream=System.out;
printStream.println("Hello world");
}
//對(duì)應(yīng)的.class文件按灶,在命令行中使用“javap -c *.class”命令查看
public void print();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: astore_1
4: aload_1
5: ldc #3 // String Hello world
7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
這個(gè)符號(hào)引用(Code的“7: invokevirtual ……”部分)包含了此方法定義在哪個(gè)具體類型之中、方法的名字以及參數(shù)順序筐咧、參數(shù)類型和方法返回值等信息鸯旁,通過(guò)這個(gè)符號(hào)引用,虛擬機(jī)就可以翻譯出這個(gè)方法的直接引用(譬如方法內(nèi)存地址或者其他實(shí)現(xiàn)形式)量蕊。而在ECMAScript等動(dòng)態(tài)類型語(yǔ)言中铺罢,變量obj本身是沒(méi)有類型的,變量obj的值才具有的類型残炮,編譯時(shí)候最多只能確定方法名稱韭赘、參數(shù)、返回值這些信息势就,而不會(huì)去確定方法所在的具體類型(方法接收者不固定)泉瞻。“變量無(wú)類型而變量值才有類型”這個(gè)特點(diǎn)也是動(dòng)態(tài)類型語(yǔ)言的一個(gè)重要特征苞冯。
Java的動(dòng)態(tài)語(yǔ)言實(shí)現(xiàn)
JDK 7時(shí)Java在虛擬機(jī)層面提供了動(dòng)態(tài)類型的直接支持:invokedynamic和java.lang.invoke包袖牙,為L(zhǎng)ambda表達(dá)式的出現(xiàn)奠定了基礎(chǔ)。
在此對(duì)invokedynamic指令做簡(jiǎn)單介紹:每一處含有invokedynamic指令的位置都被稱作“動(dòng)態(tài)調(diào)用點(diǎn)(Dynamic Call Site)”舅锄,這條指令的第一個(gè)參數(shù)不再是代表方法符號(hào)引用的CONSTANT_Methodref_info常量鞭达,而是變?yōu)镴DK 7新加入的CONSTANT_InvokeDynamic_info常量,從這個(gè)新常量中可以得到3項(xiàng)信息:引導(dǎo)方法(Bootstrap Method皇忿,此方法存放在新增的BootstrapMethods屬性中)碉怔、方法類型(MethodType)和名稱。引導(dǎo)方法是有固定的參數(shù)禁添,并且返回值是java.lang.invoke.CallSite對(duì)象撮胧,這個(gè)代表真正要執(zhí)行的目標(biāo)方法調(diào)用。根據(jù)CONSTANT_InvokeDynamic_info常量中提供的信息老翘,虛擬機(jī)可以找到并且執(zhí)行引導(dǎo)方法芹啥,從而獲得一個(gè)CallSite對(duì)象锻离,最終調(diào)用要執(zhí)行的目標(biāo)方法上。
invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V
//“#123”表示從常量池第123項(xiàng)獲取常量(CONSTANT_InvokeDynamic_info常量)
//0表示BootstrapMethods屬性表第0項(xiàng)
//其內(nèi)容則是“testMethod:(Ljava/lang/String;)V”
Lambda表達(dá)式的本質(zhì)
Java無(wú)法將方法作為參數(shù)傳遞給其他方法墓怀,因此大多數(shù)情況下會(huì)定義一個(gè)包含一個(gè)抽象方法的接口用來(lái)作為方法的載體汽纠。Lambda表達(dá)式彌補(bǔ)了這一點(diǎn),Lambda表達(dá)式的本質(zhì)是一個(gè)抽象方法傀履,可以將其作為參數(shù)傳遞虱朵。
public class MyClass {
public void print (){
new Thread(()-> {}).start(); //使用Lambda表達(dá)式
}
}
//其對(duì)應(yīng)的字節(jié)碼
public class com.example.MyClass {
public com.example.MyClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void print();
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
此為L(zhǎng)ambda表達(dá)式對(duì)應(yīng)的字節(jié)碼,可以看到這里調(diào)用run方法钓账,返回java.lang.Runnable類型的對(duì)象碴犬,然后以此構(gòu)建Thread對(duì)象。
匿名內(nèi)部類與Lambda表達(dá)式的對(duì)比
觀察匿名內(nèi)部類的實(shí)現(xiàn):
public class MyClass {
public void print (){
new Thread(new Runnable() {
@Override
public void run() {}
}).start();
}
}
//MyClass.class其對(duì)應(yīng)的字節(jié)碼
public class com.example.MyClass {
public com.example.MyClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void print();
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/MyClass$1
7: dup
8: aload_0
9: invokespecial #4 // Method com/example/MyClass$1."<init>":(Lcom/example/MyClass;)V
12: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
15: invokevirtual #6 // Method java/lang/Thread.start:()V
18: return
}
//MyClass$1.class其對(duì)應(yīng)的字節(jié)碼
class com.example.MyClass$1 implements java.lang.Runnable {
final com.example.MyClass this$0;
com.example.MyClass$1(com.example.MyClass);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/MyClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void run();
Code:
0: return
}
從中可以看出梆暮,匿名內(nèi)部類首先構(gòu)造了一個(gè)MyClass$1類的對(duì)象(該類繼承Runnable接口)服协,然后使用該對(duì)象構(gòu)造Thread對(duì)象,事實(shí)上啦粹,在MyClass.class文件所在的文件夾中同時(shí)存在MyClass$1.class文件偿荷。由此可看出,匿名內(nèi)部類的實(shí)現(xiàn)就是由編譯器構(gòu)建一個(gè)繼承對(duì)應(yīng)接口的類唠椭。
由此可得知Lambda表達(dá)式是方法跳纳,匿名內(nèi)部類是類,明白了這些贪嫂,很多事情就簡(jiǎn)單了棒旗,如:兩者如何接受外部類變量和外部方法變量?
Lambda以方法參數(shù)的形式接收:
public class MyClass {
String classString="Hellow"; //外部類變量
public void print (){
String functionString="World"; //外部方法變量
new Thread(()->{
String a=classString+functionString;
}).start();
}
}
//對(duì)應(yīng)字節(jié)碼
public com.example.MyClass();
Code:
……
public void print();
Code:
……
9: invokedynamic #6, 0 // InvokeDynamic #0:run:(Lcom/example/MyClass;Ljava/lang/String;)Ljava/lang/Runnable;
//run方法多了兩個(gè)參數(shù)撩荣,一個(gè)指向外部類铣揉,一個(gè)指向外部方法中的String變量
14: invokespecial #7 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
17: invokevirtual #8 // Method java/lang/Thread.start:()V
20: return
}
內(nèi)部類則是將變量添加入自身屬性列表:
class com.example.MyClass$1 implements java.lang.Runnable {
final java.lang.String val$functionString;
//外部方法中的String變量對(duì)應(yīng)的常量,內(nèi)部使用時(shí)才存在
final com.example.MyClass this$0;
//指向MyClass的常量一直存在餐曹,不管內(nèi)部有沒(méi)有使用外部類的屬性
com.example.MyClass$1(com.example.MyClass, java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/MyClass;
5: aload_0
6: aload_2
7: putfield #2 // Field val$functionString:Ljava/lang/String;
10: aload_0
11: invokespecial #3 // Method java/lang/Object."<init>":()V
14: return
public void run();
Code:
……
28: return
}
對(duì)比之前方法體為空的字節(jié)碼可以看出逛拱,內(nèi)部類會(huì)一直有一個(gè)指向外部類的指針,而Lambda表達(dá)式只有在使用到外部類的屬性時(shí)台猴,才會(huì)接收該參數(shù)朽合。
Q&A:
Lambda表達(dá)式和匿名類均能直接訪問(wèn)外部方法的局部變量和外部類的屬性,但訪問(wèn)時(shí)卻隱式的指定所訪問(wèn)的成員是final(訪問(wèn)外部類的屬性是通過(guò)指向外部類的指針完成的饱狂,該指針是final類型)曹步,為什么?
匿名內(nèi)部類訪問(wèn)外部成員時(shí)休讳,會(huì)將所訪問(wèn)的成員復(fù)制到類內(nèi))讲婚,其原因是所訪問(wèn)的成員與匿名內(nèi)部類生命周期的不同,當(dāng)方法執(zhí)行完畢后俊柔,局部變量就會(huì)被清理筹麸,但匿名類可能依舊存在活合,因此需要將其復(fù)制到內(nèi)部。為了保證復(fù)制品與原始變量的一致物赶,將其指定為final類型(Lambda表達(dá)式也是同樣的道理)白指。JDK 8之后,檢測(cè)條件寬松了許多酵紫,只要所使用的屬性在之后的代碼中告嘲,其值不會(huì)改變即可,不一定非要指定為final奖地,但在JDK 8之前橄唬,外部方法中的局部變量需定義為final。
參考資料:
周志明.深入理解Java虛擬機(jī)——JVM高級(jí)特性與最佳實(shí)踐