您好,登錄后才能下訂單哦!
本期教程將介紹 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(()?->?{ ?System.out.println("Hello"); ?System.out.println("Jimmy"); }).start();
當只有一條語句時,我們還可以對代碼塊進行簡寫,格式如下:
()?->?表達式
注意這里使用的是表達式,并不是語句,也就是說不需要在末尾加分號。
那么,當上面的例子中執行的語句只有一條時,可以簡寫成這樣:
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()?{ ?@Override ?public?void?onClick(View?v)?{ ?v.setVisibility(View.GONE); ?} });
從 Java 8 開始,單參函數的匿名內部類可以簡寫成如下方式:
([類名?]變量名)?->?{ ?執行語句 }
其中類名是可以省略的,因為 Lambda 表達式可以自己推斷出來。那么上面的例子可以簡寫成如下兩種方式:
view.setOnClickListener((View?v)?->?{ ?v.setVisibility(View.GONE); }); view.setOnClickListener((v)?->?{ ?v.setVisibility(View.GONE); });
單參函數甚至可以把括號去掉,官方也更建議使用這種方式:
變量名?->?{ ?執行語句 }
那么,上面的示例可以簡寫成:
view.setOnClickListener(v?->?{ ?v.setVisibility(View.GONE); });
當只有一條語句時,依然可以對代碼塊進行簡寫,格式如下:
([類名?]變量名)?->?表達式
類名和括號依然可以省略,如下:
變量名?->?表達式
那么,上面的示例可以進一步簡寫成:
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 及之前的版本,當我們對一個集合進行排序時,通常可以這么寫:
Listlist?=?Arrays.asList(1,?2,?3);Collections.sort(list,?new?Comparator()?{ ?@Override ?public?int?compare(Integer?o1,?Integer?o2)?{ ?return?o1.compareTo(o2); ?} });
從 Java 8 開始,多參函數的匿名內部類可以簡寫成如下方式:
([類名1?]變量名1,?[類名2?]變量名2[,?...])?->?{ ?執行語句 }
同樣類名可以省略,那么上面的例子可以簡寫成:
Collections.sort(list,?(Integer?o1,?Integer?o2)?->?{ ?return?o1.compareTo(o2); }); Collections.sort(list,?(o1,?o2)?->?{ ?return?o1.compareTo(o2); });
當只有一條語句時,依然可以對代碼塊進行簡寫,格式如下:
([類名1?]變量名1,?[類名2?]變量名2[,?...])?->?表達式
此時類名也是可以省略的,但括號不能省略。如果這條語句需要返回值,那么 return 關鍵字是不需要寫的。
因此,上面的示例可以進一步簡寫成:
Collections.sort(list,?(o1,?o2)?->?o1.compareTo(o2));
最后呢,這個示例還可以簡寫成這樣:
Collections.sort(list,?Integer::compareTo);
咦,這是什么特性?這就是我們下面要講的內容:方法引用。
三. 方法引用
方法引用也是一個語法糖,可以用來簡化開發。http://dalian.qd8.com.cn/yiyao/xinxi21_3710011.html
在我們使用 Lambda 表達式的時候,如果“->”的右邊要執行的表達式只是調用一個類已有的方法,那么就可以用「方法引用」來替代 Lambda 表達式。
方法引用可以分為 4 類:
引用靜態方法;
引用對象的方法;
引用類的方法;
引用構造方法。
下面按照這 4 類分別進行闡述。
3.1 引用靜態方法
當我們要執行的表達式是調用某個類的靜態方法,并且這個靜態方法的參數列表和接口里抽象函數的參數列表一一對應時,我們可以采用引用靜態方法的格式。
假如 Lambda 表達式符合如下格式:
([變量1,?變量2,?...])?->?類名.靜態方法名([變量1,?變量2,?...])
我們可以簡寫成如下格式:
類名::靜態方法名
注意這里靜態方法名后面不需要加括號,也不用加參數,因為編譯器都可以推斷出來。下面我們繼續使用 2.3 節的示例來進行說明。
首先創建一個工具類,代碼如下:
public?class?Utils?{ ?public?static?int?compare(Integer?o1,?Integer?o2)?{ ?return?o1.compareTo(o2); ?} }
注意這里的 compare() 函數的參數和 Comparable 接口的 compare() 函數的參數是一一對應的。然后一般的 Lambda 表達式可以這樣寫:
Collections.sort(list,?(o1,?o2)?->?Utils.compare(o1,?o2));
如果采用方法引用的方式,可以簡寫成這樣:
Collections.sort(list,?Utils::compare);
3.2 引用對象的方法
當我們要執行的表達式是調用某個對象的方法,并且這個方法的參數列表和接口里抽象函數的參數列表一一對應時,我們就可以采用引用對象的方法的格式。
假如 Lambda 表達式符合如下格式:
([變量1,?變量2,?...])?->?對象引用.方法名([變量1,?變量2,?...])
我們可以簡寫成如下格式:
對象引用::方法名
下面我們繼續使用 2.3 節的示例來進行說明。首先創建一個類,代碼如下:
public?class?MyClass?{ ?public?int?compare(Integer?o1,?Integer?o2)?{ ?return?o1.compareTo(o2); ?} }
當我們創建一個該類的對象,并在 Lambda 表達式中使用該對象的方法時,一般可以這么寫:
MyClass?myClass?=?new?MyClass(); Collections.sort(list,?(o1,?o2)?->?myClass.compare(o1,?o2));
注意這里函數的參數也是一一對應的,那么采用方法引用的方式,可以這樣簡寫:
MyClass?myClass?=?new?MyClass(); Collections.sort(list,?myClass::compare);
此外,當我們要執行的表達式是調用 Lambda 表達式所在的類的方法時,我們還可以采用如下格式:
this::方法名
例如我在 Lambda 表達式所在的類添加如下方法:
private?int?compare(Integer?o1,?Integer?o2)?{ ?return?o1.compareTo(o2); }
當 Lambda 表達式使用這個方法時,一般可以這樣寫:
Collections.sort(list,?(o1,?o2)?->?compare(o1,?o2));
如果采用方法引用的方式,就可以簡寫成這樣:
Collections.sort(list,?this::compare);
3.3 引用類的方法
引用類的方法所采用的參數對應形式與上兩種略有不同。如果 Lambda 表達式的“->”的右邊要執行的表達式是調用的“->”的左邊第一個參數的某個實例方法,并且從第二個參數開始(或無參)對應到該實例方法的參數列表時,就可以使用這種方法。焦作國醫胃腸醫院地址:http://jz.lieju.com/zhuankeyiyuan/37324455.htm
可能有點繞,假如我們的 Lambda 表達式符合如下格式:
(變量1[,?變量2,?...])?->?變量1.實例方法([變量2,?...])
那么我們的代碼就可以簡寫成:
變量1對應的類名::實例方法名
還是使用 2.3 節的例子, 當我們使用的 Lambda 表達式是這樣時:
Collections.sort(list,?(o1,?o2)?->?o1.compareTo(o2));
按照上面的說法,就可以簡寫成這樣:
Collections.sort(list,?Integer::compareTo);
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。一般我們可以這樣實現:
Functionfunction?=?new?Function()?{ ?@Override ?public?ArrayList?apply(Integer?n)?{ ?return?new?ArrayList(n); ?} }; List?list?=?function.apply(10);
使用 Lambda 表達式,我們一般可以這樣寫:
Functionfunction?=?n?->?new?ArrayList(n);
使用「引用構造方法」的方式,我們可以簡寫成這樣:
Functionfunction?=?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)?{ ?new?Thread(new?Runnable()?{ ?@Override ?public?void?run()?{ ?System.out.println("Hello?World"); ?} ?}).start(); ?} }
編譯之后將會產生兩個 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 表達式實現,代碼如下:
public?class?LambdaTest?{ ?public?static?void?main(String[]?args)?{ ?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 表達式和抽象函數中參數的類型。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。