(轉(zhuǎn)載)原文鏈接:http://www.reibang.com/p/2c0c73fb911f
本文將介紹 Java 8 新增的 Lambda 表達(dá)式,包括 Lambda 表達(dá)式的常見用法以及方法引用的用法,并對 Lambda 表達(dá)式的原理進(jìn)行分析玉吁,最后對 Lambda 表達(dá)式的優(yōu)缺點(diǎn)進(jìn)行一個總結(jié)冷冗。
1. 概述
Java 8 引入的 Lambda 表達(dá)式的主要作用就是簡化部分匿名內(nèi)部類的寫法角骤。
能夠使用 Lambda 表達(dá)式的一個重要依據(jù)是必須有相應(yīng)的函數(shù)接口调衰。所謂函數(shù)接口裙士,是指內(nèi)部有且僅有一個抽象方法的接口入客。
Lambda 表達(dá)式的另一個依據(jù)是類型推斷機(jī)制。在上下文信息足夠的情況下,編譯器可以推斷出參數(shù)表的類型桌硫,而不需要顯式指名夭咬。
2. 常見用法
2.1 無參函數(shù)的簡寫
無參函數(shù)就是沒有參數(shù)的函數(shù),例如 Runnable
接口的 run()
方法铆隘,其定義如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
在 Java 7 及之前版本皱埠,我們一般可以這樣使用:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
System.out.println("Jimmy");
}
}).start();
從 Java 8 開始,無參函數(shù)的匿名內(nèi)部類可以簡寫成如下方式:
() -> {
執(zhí)行語句
}
這樣接口名和函數(shù)名就可以省掉了咖驮。那么,上面的示例可以簡寫成:
new Thread(() -> {
System.out.println("Hello");
System.out.println("Jimmy");
}).start();
當(dāng)只有一條語句時训枢,我們還可以對代碼塊進(jìn)行簡寫托修,格式如下:
() -> 表達(dá)式
注意這里使用的是表達(dá)式,并不是語句恒界,也就是說不需要在末尾加分號睦刃。
那么,當(dāng)上面的例子中執(zhí)行的語句只有一條時十酣,可以簡寫成這樣:
new Thread(() -> System.out.println("Hello")).start();
2.2 單參函數(shù)的簡寫
單參函數(shù)是指只有一個參數(shù)的函數(shù)涩拙。例如 View
內(nèi)部的接口 OnClickListener
的方法 onClick(View v)
,其定義如下:
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
在 Java 7 及之前的版本耸采,我們通承四啵可能會這么使用:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setVisibility(View.GONE);
}
});
從 Java 8 開始,單參函數(shù)的匿名內(nèi)部類可以簡寫成如下方式:
([類名 ]變量名) -> {
執(zhí)行語句
}
其中類名是可以省略的虾宇,因?yàn)?Lambda 表達(dá)式可以自己推斷出來搓彻。那么上面的例子可以簡寫成如下兩種方式:
view.setOnClickListener((View v) -> {
v.setVisibility(View.GONE);
});
view.setOnClickListener((v) -> {
v.setVisibility(View.GONE);
});
單參函數(shù)甚至可以把括號去掉,官方也更建議使用這種方式:
變量名 -> {
執(zhí)行語句
}
那么嘱朽,上面的示例可以簡寫成:
view.setOnClickListener(v -> {
v.setVisibility(View.GONE);
});
當(dāng)只有一條語句時旭贬,依然可以對代碼塊進(jìn)行簡寫,格式如下:
([類名 ]變量名) -> 表達(dá)式
類名和括號依然可以省略搪泳,如下:
變量名 -> 表達(dá)式
那么稀轨,上面的示例可以進(jìn)一步簡寫成:
view.setOnClickListener(v -> v.setVisibility(View.GONE));
2.3 多參函數(shù)的簡寫
多參函數(shù)是指具有兩個及以上參數(shù)的函數(shù)。例如岸军,Comparator
接口的 compare(T o1, T o2)
方法就具有兩個參數(shù)奋刽,其定義如下:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
在 Java 7 及之前的版本,當(dāng)我們對一個集合進(jìn)行排序時凛膏,通逞蠲可以這么寫:
List<Integer> list = Arrays.asList(1, 2, 3);
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
從 Java 8 開始,多參函數(shù)的匿名內(nèi)部類可以簡寫成如下方式:
([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> {
執(zhí)行語句
}
同樣類名可以省略猖毫,那么上面的例子可以簡寫成:
Collections.sort(list, (Integer o1, Integer o2) -> {
return o1.compareTo(o2);
});
Collections.sort(list, (o1, o2) -> {
return o1.compareTo(o2);
});
當(dāng)只有一條語句時台谍,依然可以對代碼塊進(jìn)行簡寫,格式如下:
([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> 表達(dá)式
此時類名也是可以省略的,但括號不能省略趁蕊。如果這條語句需要返回值坞生,那么 return
關(guān)鍵字是不需要寫的。
因此掷伙,上面的示例可以進(jìn)一步簡寫成:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
最后呢是己,這個示例還可以簡寫成這樣:
Collections.sort(list, Integer::compareTo);
咦,這是什么特性任柜?這就是我們下面要講的內(nèi)容:方法引用卒废。
3. 方法引用
方法引用也是一個語法糖,可以用來簡化開發(fā)宙地。
在我們使用 Lambda 表達(dá)式的時候摔认,如果“->”的右邊要執(zhí)行的表達(dá)式只是調(diào)用一個類已有的方法,那么就可以用「方法引用」來替代 Lambda 表達(dá)式宅粥。
方法引用可以分為 4 類:
- 引用靜態(tài)方法参袱;
- 引用對象的方法;
- 引用類的方法秽梅;
- 引用構(gòu)造方法抹蚀。
下面按照這 4 類分別進(jìn)行闡述。
3.1 引用靜態(tài)方法
當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用某個類的靜態(tài)方法企垦,并且這個靜態(tài)方法的參數(shù)列表和接口里抽象函數(shù)的參數(shù)列表一一對應(yīng)時环壤,我們可以采用引用靜態(tài)方法的格式。
假如 Lambda 表達(dá)式符合如下格式:
([變量1, 變量2, ...]) -> 類名.靜態(tài)方法名([變量1, 變量2, ...])
我們可以簡寫成如下格式:
類名::靜態(tài)方法名
注意這里靜態(tài)方法名后面不需要加括號钞诡,也不用加參數(shù)镐捧,因?yàn)榫幾g器都可以推斷出來。下面我們繼續(xù)使用 2.3 節(jié)的示例來進(jìn)行說明臭增。
首先創(chuàng)建一個工具類懂酱,代碼如下:
public class Utils {
public static int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
注意這里的 compare()
函數(shù)的參數(shù)和 Comparable
接口的 compare()
函數(shù)的參數(shù)是一一對應(yīng)的。然后一般的 Lambda 表達(dá)式可以這樣寫:
Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));
如果采用方法引用的方式誊抛,可以簡寫成這樣:
Collections.sort(list, Utils::compare);
3.2 引用對象的方法
當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用某個對象的方法列牺,并且這個方法的參數(shù)列表和接口里抽象函數(shù)的參數(shù)列表一一對應(yīng)時,我們就可以采用引用對象的方法的格式拗窃。
假如 Lambda 表達(dá)式符合如下格式:
([變量1, 變量2, ...]) -> 對象引用.方法名([變量1, 變量2, ...])
我們可以簡寫成如下格式:
對象引用::方法名
下面我們繼續(xù)使用 2.3 節(jié)的示例來進(jìn)行說明瞎领。首先創(chuàng)建一個類,代碼如下:
public class MyClass {
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
當(dāng)我們創(chuàng)建一個該類的對象随夸,并在 Lambda 表達(dá)式中使用該對象的方法時九默,一般可以這么寫:
MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));
注意這里函數(shù)的參數(shù)也是一一對應(yīng)的,那么采用方法引用的方式宾毒,可以這樣簡寫:
MyClass myClass = new MyClass();
Collections.sort(list, myClass::compare);
此外驼修,當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用 Lambda 表達(dá)式所在的類的方法時,我們還可以采用如下格式:
this::方法名
例如我在 Lambda 表達(dá)式所在的類添加如下方法:
private int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
當(dāng) Lambda 表達(dá)式使用這個方法時,一般可以這樣寫:
Collections.sort(list, (o1, o2) -> compare(o1, o2));
如果采用方法引用的方式乙各,就可以簡寫成這樣:
Collections.sort(list, this::compare);
3.3 引用類的方法
引用類的方法所采用的參數(shù)對應(yīng)形式與上兩種略有不同墨礁。如果 Lambda 表達(dá)式的“->”的右邊要執(zhí)行的表達(dá)式是調(diào)用的“->”的左邊第一個參數(shù)的某個實(shí)例方法,并且從第二個參數(shù)開始(或無參)對應(yīng)到該實(shí)例方法的參數(shù)列表時耳峦,就可以使用這種方法恩静。
可能有點(diǎn)繞,假如我們的 Lambda 表達(dá)式符合如下格式:
(變量1[, 變量2, ...]) -> 變量1.實(shí)例方法([變量2, ...])
那么我們的代碼就可以簡寫成:
變量1對應(yīng)的類名::實(shí)例方法名
還是使用 2.3 節(jié)的例子蹲坷, 當(dāng)我們使用的 Lambda 表達(dá)式是這樣時:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
按照上面的說法驶乾,就可以簡寫成這樣:
Collections.sort(list, Integer::compareTo);
3.4 引用構(gòu)造方法
當(dāng)我們要執(zhí)行的表達(dá)式是新建一個對象,并且這個對象的構(gòu)造方法的參數(shù)列表和接口里函數(shù)的參數(shù)列表一一對應(yīng)時循签,我們就可以采用「引用構(gòu)造方法」的格式轻掩。
假如我們的 Lambda 表達(dá)式符合如下格式:
([變量1, 變量2, ...]) -> new 類名([變量1, 變量2, ...])
我們就可以簡寫成如下格式:
類名::new
下面舉個例子說明一下。Java 8 引入了一個 Function
接口懦底,它是一個函數(shù)接口,部分代碼如下:
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
// 省略部分代碼
}
我們用這個接口來實(shí)現(xiàn)一個功能罕扎,創(chuàng)建一個指定大小的 ArrayList
聚唐。一般我們可以這樣實(shí)現(xiàn):
Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() {
@Override
public ArrayList apply(Integer n) {
return new ArrayList(n);
}
};
List list = function.apply(10);
使用 Lambda 表達(dá)式,我們一般可以這樣寫:
Function<Integer, ArrayList> function = n -> new ArrayList(n);
使用「引用構(gòu)造方法」的方式腔召,我們可以簡寫成這樣:
Function<Integer, ArrayList> function = ArrayList::new;
4. 自定義函數(shù)接口
自定義函數(shù)接口很容易杆查,只需要編寫一個只有一個抽象方法的接口即可,示例代碼:
@FunctionalInterface
public interface MyInterface<T> {
void function(T t);
}
上面代碼中的 @FunctionalInterface
是可選的臀蛛,但加上該注解編譯器會幫你檢查接口是否符合函數(shù)接口規(guī)范亲桦。就像加入 @Override
注解會檢查是否重寫了函數(shù)一樣。
5. 實(shí)現(xiàn)原理
經(jīng)過上面的介紹浊仆,我們看到 Lambda 表達(dá)式只是為了簡化匿名內(nèi)部類書寫客峭,看起來似乎在編譯階段把所有的 Lambda 表達(dá)式替換成匿名內(nèi)部類就可以了。但實(shí)際情況并非如此抡柿,在 JVM 層面舔琅,Lambda 表達(dá)式和匿名內(nèi)部類其實(shí)有著明顯的差別。
5.1 匿名內(nèi)部類的實(shí)現(xiàn)
匿名內(nèi)部類仍然是一個類洲劣,只是不需要我們顯式指定類名备蚓,編譯器會自動為該類取名。比如有如下形式的代碼:
public class LambdaTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
}
}
編譯之后將會產(chǎn)生兩個 class 文件:
LambdaTest.class
LambdaTest$1.class
使用 javap -c LambdaTest.class
進(jìn)一步分析 LambdaTest.class
的字節(jié)碼囱稽,部分結(jié)果如下:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/myapplication/lambda/LambdaTest$1
7: dup
8: invokespecial #4 // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
可以發(fā)現(xiàn)在 4: new #3
這一行創(chuàng)建了匿名內(nèi)部類的對象郊尝。
5.2 Lambda 表達(dá)式的實(shí)現(xiàn)
接下來我們將上面的示例代碼使用 Lambda 表達(dá)式實(shí)現(xiàn),代碼如下:
public class LambdaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World")).start();
}
}
此時編譯后只會產(chǎn)生一個文件 LambdaTest.class
战惊,再來看看通過 javap 對該文件反編譯后的結(jié)果:
public static void main(java.lang.String[]);
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
從上面的結(jié)果我們發(fā)現(xiàn) Lambda 表達(dá)式被封裝成了主類的一個私有方法流昏,并通過 invokedynamic
指令進(jìn)行調(diào)用。
因此,我們可以得出結(jié)論:Lambda 表達(dá)式是通過 invokedynamic
指令實(shí)現(xiàn)的横缔,并且書寫 Lambda 表達(dá)式不會產(chǎn)生新的類铺遂。
既然 Lambda 表達(dá)式不會創(chuàng)建匿名內(nèi)部類,那么在 Lambda 表達(dá)式中使用 this
關(guān)鍵字時茎刚,其指向的是外部類的引用襟锐。
6. 優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 可以減少代碼的書寫,減少匿名內(nèi)部類的創(chuàng)建膛锭,節(jié)省內(nèi)存占用粮坞。
- 使用時不用去記憶所使用的接口和抽象函數(shù)。
缺點(diǎn):
- 易讀性較差初狰,閱讀代碼的人需要熟悉 Lambda 表達(dá)式和抽象函數(shù)中參數(shù)的類型莫杈。
- 不方便進(jìn)行調(diào)試。