本期教程将介紹 Java 8 新增的 Lambda 表達式,包括 Lambda 表達式的常見用法以及方法引用的用法,并對 Lambda 表達式的原理進行分析,最後對 Lambda 表達式的優缺點進行一個總結。
概述
Java 8 引入的 Lambda 表達式的主要作用就是簡化部分匿名内部類的寫法。
能夠使用 Lambda 表達式的一個重要依據是必須有相應的函數接口。所謂函數接口,是指内部有且僅有一個抽象方法的接口。
Lambda 表達式的另一個依據是類型推斷機制。在上下文資訊足夠的情況下,編譯器可以推斷出參數表的類型,而不需要顯式指名。
常見用法
2.1 無參函數的簡寫
無參函數就是沒有參數的函數,例如 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 開始,無參函數的匿名内部類可以簡寫成如下方式:
() -> {
執行語句
這樣接口名和函數名就可以省掉了。那麼,上面的示例可以簡寫成:
new Thread(() -> {
當隻有一條語句時,我們還可以對代碼塊進行簡寫,格式如下:
() -> 表達式
注意這裡使用的是表達式,并不是語句,也就是說不需要在末尾加分号。
那麼,當上面的例子中執行的語句隻有一條時,可以簡寫成這樣:
new Thread(() -> System.out.println("Hello")).start();
2.2 單參函數的簡寫
單參函數是指隻有一個參數的函數。例如 View 内部的接口 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() {
public void onClick(View v) {
v.setVisibility(View.GONE);
});
從 Java 8 開始,單參函數的匿名内部類可以簡寫成如下方式:
([類名 ]變量名) -> {
其中類名是可以省略的,因為 Lambda 表達式可以自己推斷出來。那麼上面的例子可以簡寫成如下兩種方式:
view.setOnClickListener((View v) -> {
view.setOnClickListener((v) -> {
單參函數甚至可以把括号去掉,官方也更建議使用這種方式:
變量名 -> {
那麼,上面的示例可以簡寫成:
view.setOnClickListener(v -> {
當隻有一條語句時,依然可以對代碼塊進行簡寫,格式如下:
([類名 ]變量名) -> 表達式
類名和括号依然可以省略,如下:
變量名 -> 表達式
那麼,上面的示例可以進一步簡寫成:
view.setOnClickListener(v -> v.setVisibility(View.GONE));
2.3 多參函數的簡寫
多參函數是指具有兩個及以上參數的函數。例如,Comparator 接口的 compare(T o1, T o2) 方法就具有兩個參數,其定義如下:
@FunctionalInterfacepublic interface Comparator {
int compare(T o1, T o2);
在 Java 7 及之前的版本,當我們對一個集合進行排序時,通常可以這麼寫:
List list = Arrays.asList(1, 2, 3);Collections.sort(list, new Comparator() {
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
從 Java 8 開始,多參函數的匿名内部類可以簡寫成如下方式:
([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> {
同樣類名可以省略,那麼上面的例子可以簡寫成:
Collections.sort(list, (Integer o1, Integer o2) -> {
Collections.sort(list, (o1, o2) -> {
([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> 表達式
此時類名也是可以省略的,但括号不能省略。如果這條語句需要傳回值,那麼 return 關鍵字是不需要寫的。
是以,上面的示例可以進一步簡寫成:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
最後呢,這個示例還可以簡寫成這樣:
Collections.sort(list, Integer::compareTo);
咦,這是什麼特性?這就是我們下面要講的内容:方法引用。
三. 方法引用
方法引用也是一個文法糖,可以用來簡化開發。
在我們使用 Lambda 表達式的時候,如果“->”的右邊要執行的表達式隻是調用一個類已有的方法,那麼就可以用「方法引用」來替代 Lambda 表達式。
方法引用可以分為 4 類:
引用靜态方法;
引用對象的方法;
引用類的方法;
引用構造方法。
下面按照這 4 類分别進行闡述。
3.1 引用靜态方法
當我們要執行的表達式是調用某個類的靜态方法,并且這個靜态方法的參數清單和接口裡抽象函數的參數清單一一對應時,我們可以采用引用靜态方法的格式。
假如 Lambda 表達式符合如下格式:
([變量1, 變量2, ...]) -> 類名.靜态方法名([變量1, 變量2, ...])
我們可以簡寫成如下格式:
類名::靜态方法名
注意這裡靜态方法名後面不需要加括号,也不用加參數,因為編譯器都可以推斷出來。下面我們繼續使用 2.3 節的示例來進行說明。
首先建立一個工具類,代碼如下:
public class Utils {
public static int compare(Integer o1, Integer o2) {
注意這裡的 compare() 函數的參數和 Comparable 接口的 compare() 函數的參數是一一對應的。然後一般的 Lambda 表達式可以這樣寫:
Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));
如果采用方法引用的方式,可以簡寫成這樣:
Collections.sort(list, Utils::compare);
3.2 引用對象的方法
當我們要執行的表達式是調用某個對象的方法,并且這個方法的參數清單和接口裡抽象函數的參數清單一一對應時,我們就可以采用引用對象的方法的格式。
([變量1, 變量2, ...]) -> 對象引用.方法名([變量1, 變量2, ...])
對象引用::方法名
下面我們繼續使用 2.3 節的示例來進行說明。首先建立一個類,代碼如下:
public class MyClass {
當我們建立一個該類的對象,并在 Lambda 表達式中使用該對象的方法時,一般可以這麼寫:
MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));
注意這裡函數的參數也是一一對應的,那麼采用方法引用的方式,可以這樣簡寫:
Collections.sort(list, myClass::compare);
此外,當我們要執行的表達式是調用 Lambda 表達式所在的類的方法時,我們還可以采用如下格式:
this::方法名
例如我在 Lambda 表達式所在的類添加如下方法:
private int compare(Integer o1, Integer o2) {
當 Lambda 表達式使用這個方法時,一般可以這樣寫:
Collections.sort(list, (o1, o2) -> compare(o1, o2));
如果采用方法引用的方式,就可以簡寫成這樣:
Collections.sort(list, this::compare);
3.3 引用類的方法
引用類的方法所采用的參數對應形式與上兩種略有不同。如果 Lambda 表達式的“->”的右邊要執行的表達式是調用的“->”的左邊第一個參數的某個執行個體方法,并且從第二個參數開始(或無參)對應到該執行個體方法的參數清單時,就可以使用這種方法。
可能有點繞,假如我們的 Lambda 表達式符合如下格式:
(變量1[, 變量2, ...]) -> 變量1.執行個體方法([變量2, ...])
那麼我們的代碼就可以簡寫成:
變量1對應的類名::執行個體方法名
還是使用 2.3 節的例子, 當我們使用的 Lambda 表達式是這樣時:
按照上面的說法,就可以簡寫成這樣:
3.4 引用構造方法
當我們要執行的表達式是建立一個對象,并且這個對象的構造方法的參數清單和接口裡函數的參數清單一一對應時,我們就可以采用「引用構造方法」的格式。
假如我們的 Lambda 表達式符合如下格式:
([變量1, 變量2, ...]) -> new 類名([變量1, 變量2, ...])
我們就可以簡寫成如下格式:
類名::new
下面舉個例子說明一下。Java 8 引入了一個 Function 接口,它是一個函數接口,部分代碼如下:
@FunctionalInterfacepublic interface Function {
- Applies this function to the given argument.
- @param t the function argument
- @return the function result
R apply(T t);
// 省略部分代碼
我們用這個接口來實作一個功能,建立一個指定大小的 ArrayList。一般我們可以這樣實作:
Function function = new Function() {
public ArrayList apply(Integer n) {
return new ArrayList(n);
};
List list = function.apply(10);
使用 Lambda 表達式,我們一般可以這樣寫:
Function function = n -> new ArrayList(n);
使用「引用構造方法」的方式,我們可以簡寫成這樣:
Function function = ArrayList::new;
四. 自定義函數接口
自定義函數接口很容易,隻需要編寫一個隻有一個抽象方法的接口即可,示例代碼:
@FunctionalInterfacepublic interface MyInterface {
void function(T t);
上面代碼中的 @FunctionalInterface 是可選的,但加上該注解編譯器會幫你檢查接口是否符合函數接口規範。就像加入 @Override 注解會檢查是否重寫了函數一樣。
五. 實作原理
經過上面的介紹,我們看到 Lambda 表達式隻是為了簡化匿名内部類書寫,看起來似乎在編譯階段把所有的 Lambda 表達式替換成匿名内部類就可以了。但實際情況并非如此,在 JVM 層面,Lambda 表達式和匿名内部類其實有着明顯的差别。
5.1 匿名内部類的實作
匿名内部類仍然是一個類,隻是不需要我們顯式指定類名,編譯器會自動為該類取名。比如有如下形式的代碼:
public class LambdaTest {
public static void main(String[] args) {
System.out.println("Hello World");
編譯之後将會産生兩個 class 檔案:
LambdaTest.class
LambdaTest$1.class
使用 javap -c LambdaTest.class 進一步分析 LambdaTest.class 的位元組碼,部分結果如下:
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."":()V 11: invokespecial #5 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
可以發現在 4: new #3 這一行建立了匿名内部類的對象。
5.2 Lambda 表達式的實作
接下來我們将上面的示例代碼使用 Lambda 表達式實作,代碼如下:
new Thread(() -> System.out.println("Hello World")).start();
此時編譯後隻會産生一個檔案 LambdaTest.class,再來看看通過 javap 對該檔案反編譯後的結果:
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."":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
從上面的結果我們發現 Lambda 表達式被封裝成了主類的一個私有方法,并通過 invokedynamic 指令進行調用。
是以,我們可以得出結論:Lambda 表達式是通過 invokedynamic 指令實作的,并且書寫 Lambda 表達式不會産生新的類。
既然 Lambda 表達式不會建立匿名内部類,那麼在 Lambda 表達式中使用 this 關鍵字時,其指向的是外部類的引用。
**六. 優缺點
**
優點:
可以減少代碼的書寫,減少匿名内部類的建立,節省記憶體占用。
使用時不用去記憶所使用的接口和抽象函數。
缺點:
易讀性較差,閱讀代碼的人需要熟悉 Lambda 表達式和抽象函數中參數的類型。