本文將介紹 Java 8 新增的 Lambda 表達式,包括 Lambda 表達式的常見用法以及方法引用的用法,並對 Lambda 表達式的原理進行分析,最後對 Lambda 表達式的優缺點進行一個總結。前端
Java 8 引入的 Lambda 表達式的主要做用就是簡化部分匿名內部類的寫法。java
可以使用 Lambda 表達式的一個重要依據是必須有相應的函數接口。所謂函數接口,是指內部有且僅有一個抽象方法的接口。app
Lambda 表達式的另外一個依據是類型推斷機制。在上下文信息足夠的狀況下,編譯器能夠推斷出參數表的類型,而不須要顯式指名。ide
無參函數就是沒有參數的函數,例如 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 開始,無參函數的匿名內部類能夠簡寫成以下方式:學習
()this
() -> {
執行語句
}
這樣接口名和函數名就能夠省掉了。那麼,上面的示例能夠簡寫成:spa
new Thread(() -> { System.out.println("Hello"); System.out.println("Jimmy"); }).start();
當只有一條語句時,咱們還能夠對代碼塊進行簡寫,格式以下:調試
() -> 表達式
注意這裏使用的是表達式,並非語句,也就是說不須要在末尾加分號。
那麼,當上面的例子中執行的語句只有一條時,能夠簡寫成這樣:
new Thread(() -> System.out.println("Hello")).start();
單參函數是指只有一個參數的函數。例如 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);
});
當只有一條語句時,依然能夠對代碼塊進行簡寫,格式以下:
([類名 ]變量名) -> 表達式
1
類名和括號依然能夠省略,以下:
變量名 -> 表達式
1
那麼,上面的示例能夠進一步簡寫成:
view.setOnClickListener(v -> v.setVisibility(View.GONE));
多參函數是指具備兩個及以上參數的函數。例如,Comparator 接口的 compare(T o1, T o2) 方法就具備兩個參數,其定義以下:
FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); }
在 Java 7 及以前的版本,當咱們對一個集合進行排序時,一般能夠這麼寫:
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 開始,多參函數的匿名內部類能夠簡寫成以下方式:
([類名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[, ...]) -> 表達式
1
此時類名也是能夠省略的,但括號不能省略。若是這條語句須要返回值,那麼 return 關鍵字是不須要寫的。
所以,上面的示例能夠進一步簡寫成:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
最後呢,這個示例還能夠簡寫成這樣:
Collections.sort(list, Integer::compareTo);
咦,這是什麼特性?這就是咱們下面要講的內容:方法引用。
相信不少人在剛接觸前端或者中期時候總會遇到一些問題及瓶頸期,如學了一段時間沒有方向感或者堅持不下去一我的學習枯燥乏味有問題也不知道怎麼解決,對此我整理了一些資料 想要獲得更多的提高和知識想與更多資深大牛一塊兒討論和學習的話 歡迎加入個人學習交流羣907694362
方法引用也是一個語法糖,能夠用來簡化開發。
在咱們使用 Lambda 表達式的時候,若是「->」的右邊要執行的表達式只是調用一個類已有的方法,那麼就能夠用「方法引用」來替代 Lambda 表達式。
方法引用能夠分爲 4 類:
引用靜態方法;
引用對象的方法;
引用類的方法;
引用構造方法。
下面按照這 4 類分別進行闡述。
當咱們要執行的表達式是調用某個類的靜態方法,而且這個靜態方法的參數列表和接口裏抽象函數的參數列表一一對應時,咱們能夠採用引用靜態方法的格式。
假如 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);
當咱們要執行的表達式是調用某個對象的方法,而且這個方法的參數列表和接口裏抽象函數的參數列表一一對應時,咱們就能夠採用引用對象的方法的格式。
假如 Lambda 表達式符合以下格式:
([變量1, 變量2, ...]) -> 對象引用.方法名([變量1, 變量2, ...])
1
咱們能夠簡寫成以下格式:
對象引用::方法名
1
下面咱們繼續使用 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);
private int compare(Integer o1, Integer o2) { return o1.compareTo(o2); }
引用類的方法所採用的參數對應形式與上兩種略有不一樣。若是 Lambda 表達式的「->」的右邊要執行的表達式是調用的「->」的左邊第一個參數的某個實例方法,而且從第二個參數開始(或無參)對應到該實例方法的參數列表時,就可使用這種方法。
可能有點繞,假如咱們的 Lambda 表達式符合以下格式:
(變量1[, 變量2, ...]) -> 變量1.實例方法([變量2, ...])
1
那麼咱們的代碼就能夠簡寫成:
變量1對應的類名::實例方法名
1
仍是使用 2.3 節的例子, 當咱們使用的 Lambda 表達式是這樣時:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
按照上面的說法,就能夠簡寫成這樣:
Collections.sort(list, Integer::compareTo);
當咱們要執行的表達式是新建一個對象,而且這個對象的構造方法的參數列表和接口裏函數的參數列表一一對應時,咱們就能夠採用「引用構造方法」的格式。
假如咱們的 Lambda 表達式符合以下格式:
([變量1, 變量2, ...]) -> new 類名([變量1, 變量2, ...])
1
咱們就能夠簡寫成以下格式:
類名::new
下面舉個例子說明一下。Java 8 引入了一個 Function 接口,它是一個函數接口,部分代碼以下:
@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); // 省略部分代碼 }
咱們用這個接口來實現一個功能,建立一個指定大小的 ArrayList。通常咱們能夠這樣實現:
Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() { @Override public ArrayList apply(Integer n) { return new ArrayList(n); } }; List list = function.apply(10);
使用 Lambda 表達式,咱們通常能夠這樣寫:
Function<Integer, ArrayList> function = n -> new ArrayList(n); 1
使用「引用構造方法」的方式,咱們能夠簡寫成這樣:
Function<Integer, ArrayList> function = ArrayList::new;
4. 自定義函數接口
自定義函數接口很容易,只須要編寫一個只有一個抽象方法的接口便可,示例代碼:
@FunctionalInterface public interface MyInterface<T> { 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 1 2
使用 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."<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
能夠發如今 4: new #3 這一行建立了匿名內部類的對象。
接下來咱們將上面的示例代碼使用 Lambda 表達式實現,代碼以下:
public class LambdaTest { public static void main(String[] args) { new Thread(() -> System.out.println("Hello World")).start(); } }
此時編譯後只會產生一個文件 LambdaTest.class,再來看看經過 javap 對該文件反編譯後的結果:
ublic 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
從上面的結果咱們發現 Lambda 表達式被封裝成了主類的一個私有方法,並經過 invokedynamic 指令進行調用。
所以,咱們能夠得出結論:Lambda 表達式是經過 invokedynamic 指令實現的,而且書寫 Lambda 表達式不會產生新的類。
既然 Lambda 表達式不會建立匿名內部類,那麼在 Lambda 表達式中使用 this 關鍵字時,其指向的是外部類的引用。
優勢:
能夠減小代碼的書寫,減小匿名內部類的建立,節省內存佔用。
使用時不用去記憶所使用的接口和抽象函數。
缺點:
易讀性較差,閱讀代碼的人須要熟悉 Lambda 表達式和抽象函數中參數的類型。
不方便進行調試。
相信不少人在剛接觸前端或者中期時候總會遇到一些問題及瓶頸期,如學了一段時間沒有方向感或者堅持不下去一我的學習枯燥乏味有問題也不知道怎麼解決,對此我整理了一些資料 想要獲得更多的提高和知識想與更多資深大牛一塊兒討論和學習的話 歡迎加入個人學習交流羣907694362