Java SE基礎鞏固(十五):lambda表示式
1 概述
Java8據說是Java誕生以來最大的一次演進,說實話,對我個人來說沒有什麼特別大的感受,因為我學Java也就最近一兩年的事,Java8在2014年3月18日釋出,新增的特性確實非常驚豔,在語言特性層面上新增了lambda,Optional,預設方法,Stream API等,在虛擬機器器層面上新增了G1收集器(不過在Java9之後才改為預設的垃圾收集器)......
我個人認為Java8和語言相關的幾個最重要的特性是如下幾個:
- lambda表示式和方法引用(其實是lambda表示式的一種特例)
- Stream API
- 介面的預設方法
- Optinal
- CompletableFuture
本系列文章的後面幾篇文章會圍繞這幾個主題來展開,今天就先上個開胃菜,lambda表示式!
2 什麼是lambda表示式
lambda表示式也叫做匿名函式,其基於著名的λ演算得名,關於λ演算,推薦大家去找找關於“丘奇數”相關的資料。Java一直被人詬病的一點就是“囉嗦”,通常為了實現一個小功能,就不得不編寫大量的程式碼,而用其他的語言例如Python等,也許寥寥幾行程式碼就解決了,但支援lambda表示式之後,這一情況得到了大大的改善,現在只要使用得當,可以大大縮減程式碼裡,使程式碼的目的更加清晰,易讀,純粹。
在Java中,很多時候在使用一些API的時候,必須要給出一些介面的實現,但因為該實現其實也就用一次,專門去建立一個新的實現類並不划算,所以一般大多數人採取的措施應該是建立一個匿名實現類,比較典型就是Collections.sort(List list,Comparator<? super T> c)方法,該方法接受一個Comparator型別的引數,Comparator是一個介面,表示“比較器”,如果要使用該方法對集合元素進行排序,就必須提供一個Comparator介面的實現,否則無法通過編譯。如下所示:
Collections.sort(numbers,new Comparator<Integer>() {
@Override
public int compare(Integer o1,Integer o2) {
return o1.compareTo(o2);
}
});
複製程式碼
其實這個實現類的核心只有一行,即return o1.compareTo(o2);但我們卻不得不編寫其他“囉嗦”的程式碼,如果使用lambda表示式,會是怎麼個樣子呢?
Collections.sort(numbers,(n1,n2) -> n1.compareTo(n2));
複製程式碼
沒錯,就是那麼簡單粗暴,就是一行核心程式碼。其他的比如方法簽名啥的統統可以省略了,不僅簡潔,而且語義也更加清晰,讀起來就好像是說:“sort方法,幫我吧numbers這個序列排個序,排序規則就按照n1.compareTo(n2)的返回值來決定”。現在,是不是感覺,寫程式碼就像在和計算機對話一樣簡單?但(n1,n2) -> n1.compareTo(n2)這玩意是個什麼鬼?還帶個箭頭?不用著急,下面馬上介紹lambda表示式的語法。
2.1 lambda表示式的語法
- 第一部分是lambda的引數列表,因為Comparator.compare()方法接受兩個引數,所以這裡給出兩個引數n1和n2,可以省略具體的型別,Java編譯器會自動推斷。
- 第二部分是箭頭,沒什麼特殊的地方,只是Java語言覺得使用這個,各個語言的實現也不太一樣,例如Python是:號,簡單理解的就當是把引數列表和函式主體分開的東西吧。
- 第三部分就是函式主體,也就是真正執行邏輯的地方。
如果函式主體僅僅包含一行程式碼,可以省略花括號{}和return關鍵字(如果有的話)。對於我們的例子,可以改寫成這樣:
Collections.sort(numbers,n2) -> {return n1.compareTo(n2);});
複製程式碼
注意分號!因為此時return n1.compareTo(n2);就是一條普通的Java語句了,必須遵守Java的語法規則。好了,儘管我們現在明白了lambda語句的語法規則,但還有一個關鍵的問題,就是為什麼要這樣寫,換句話說,為什麼要有倆引數,這return又是幾個意思?還有到底哪裡才可以使用lambda表示式?說到這,就不得不說一下和lambda息息相關的東西了:函式式介面。
3 函式式介面
函式式介面是這樣的:只有一個抽象方法的介面就是函式式介面。為什麼要特別強調抽象方法呢?Java介面裡宣告的方法不都是抽象方法嗎?在Java8之前,這麼說確實沒有任何問題,但Java8新增了介面的預設方法,可以在介面裡給出方法的具體實現,這裡先不多說,後面的文章會詳細討論這個東西。
lambda表示式僅可以用在函式式介面上,我們在上面遇到的Comparator就是一個函式式介面,他只有一個抽象方法:compare(),其方法簽名是這樣的:
int compare(T o1,T o2);
複製程式碼
現在來看看 (n1,n2) -> n1.compareTo(n2)這個表示式,是不是發現了什麼?沒錯,其實lambda表示式的引數列表就是對應的函式式介面的抽象方法的引數列表,並且型別可以省略(編譯器自動推斷),然後n1.compareTo(n2)的返回值是int型別,也符合compare()的方法描述。這樣就算是把lambda表示式和介面的抽象方法簽名匹配成功了,不會出現編譯錯誤。
除此之外,Runnable也是一個函式式介面,它只有一個抽象方法,即run(),run()方法的方法簽名如下所示:
public abstract void run();
複製程式碼
不接受任何引數,也沒有返回值。那如果要編寫對應的lambda表示式,該如何做呢?其實非常簡單,下面是一個示例:
Runnable r = () -> {
System.out.println(Thread.currentThread().getName());
//do something
};
複製程式碼
如果觀察仔細的話,會發現,示例程式碼中把這個lambda表示式賦值給了Runnable型別的變數r!經過上面的討論,我們知道,其實lambda就是一個方法實現(其實叫做函式會更加合適),這條賦值語句看起來就好像是再說:“把方法(函式)賦值給變數!”。如果沒有接觸過函式語言程式設計,會覺得這樣很奇怪,怎麼能把方法賦值給變數呢?計算機就是這樣有意思,總是有各種各樣奇奇怪怪的東西衝擊我們的思維!那這有什麼用呢?咱先不說什麼高階函式,科裡化啥的(這些是函式語言程式設計裡的概念),就說一點:意味著我們可以把方法(函式)當做變數來使用!即現在方法就是Java世界裡的“一等公民”了!既可以將其作為引數傳遞給其他方法(函式),還可以將其作為其他方法(函式)的返回值(以後會講到具體的案例)
4 策略模式
策略模式是著名的23種設計模式中的一種,關於它的描述,我這裡就不多說了。直接來看個例子吧。
例子是這樣的,現在有一個代表汽車的Car類以及一個Car列表,現在我們想要篩選列表中符合要求的汽車,為了應對多變的篩選方法,我們打算用策略模式來實現功能。
下面是Car類的程式碼:
public class Car {
//品牌
private String brand;
//顏色
private Color color;
//車齡
private Integer age;
//三個引數的建構函式以及setter和getter
//顏色的列舉
public enum Color {
RED,WHITE,PINK,BLACK,BLUE;
}
}
//包含Car物件的列表
List<Car> cars = Arrays.asList(
new Car("BWM",Car.Color.BLACK,2),new Car("Tesla",Car.Color.WHITE,1),new Car("BENZ",Car.Color.RED,3),new Car("Maserati",new Car("Audi",Car.Color.PINK,5));
複製程式碼
我們希望用一個方法來封裝篩選的邏輯,其方法簽名虛擬碼如下所示:
cars carFilter(cars,filterStrategy);
複製程式碼
接下來實現策略模式,下面是相關的程式碼:
public interface CarFilterStrategy {
boolean filter(Car car);
}
public class BWMCarFilterStrategy implements CarFilterStrategy {
@Override
public boolean filter(Car car) {
return "BWM".equals(car.getBrand());
}
}
public class RedColorCarFilterStrategy implements CarFilterStrategy {
@Override
public boolean filter(Car car) {
return Car.Color.RED.equals(car.getColor());
}
}
複製程式碼
為了簡單,僅僅實現了兩種篩選策略,第一種是刪選出品牌是“BWM”的汽車,第二種是刪選出顏色為紅色的汽車。最後來實現carFilter方法,如下所示:
private static List<Car> carFilter(List<Car> cars,CarFilterStrategy strategy) {
List<Car> filteredCars = new ArrayList<>();
for (Car car : cars) {
if (strategy.filter(car)) {
filteredCars.add(car);
}
}
return filteredCars;
}
複製程式碼
最後的最後是測試程式碼:
public static void main(String[] args) {
System.out.println(carFilter(cars,new BWMCarFilterStrategy()));
System.out.println("----------------------------------------");
System.out.println(carFilter(cars,new RedColorCarFilterStrategy()));
}
複製程式碼
分別例項化兩個策略,將其作為引數傳遞給carFilter()方法,最終的輸出如下所示:
[Car{brand='BWM',color=BLACK,age=2}]
----------------------------------------
[Car{brand='BENZ',color=RED,age=3}]
複製程式碼
確實符合預期。是不是就到此為止了呢?當然不!我們發現,其實BWMCarFilterStrategy以及RedColorCarFilterStrategy的實現程式碼都非常簡單,僅僅寥寥幾行程式碼,而且CarFilterStrategy介面僅僅有一個filter抽象方法,顯然是一個函式式介面,那我們能不能用lambda表示式來簡化呢?答案是:完全可以!而且更加推薦用lambda表示式來簡化這種情況。
4.1 用lambda表示式來簡化程式碼
只要略微做一些修改就行了:
System.out.println(carFilter(cars,car -> "BWM".equals(car.getBrand())));
System.out.println("----------------------------------------");
System.out.println(carFilter(cars,car -> Car.Color.RED.equals(car.getColor())));
複製程式碼
這裡不再使用BWMCarFilterStrategy以及RedColorCarFilterStrategy兩個類了,直接用lambda表示式就行了!最後把這倆實現刪除掉!是不是頓時感覺整個專案的程式碼清爽了許多?
4.2 需要注意的
其實本小節的例子有些過於特殊了,如果你專案中的策略模式的實現非常複雜,其策略不是簡簡單單的幾行程式碼就能解決的,此時要麼進一步封裝程式碼,要麼就最好不要用lambda表示式了,因為如果邏輯複雜的話,強行使用lambda不僅僅不能簡化程式碼,反而會使得程式碼更加晦澀。
5 方法引用
最後簡單講一下方法引用吧,方法引用其實是lambda表示式的一種特殊情況的表示,語法規則是:
<class name or instance name>:<method name>
複製程式碼
如果lambda表示式的主體邏輯僅僅是一個呼叫方法的語句的話,那麼就可以將其轉換為方法引用,如下所示:
//普通的lambda表示式
numbers.forEach(n -> System.out.println(n));
//轉換成方法引用
numbers.forEach(System.out::println);
複製程式碼
他倆效果是完全一樣的,但顯然方法引用更加簡潔,語義也更加明確了,這一語法糖“真香!”。具體的我就不多說了,建議看看《Java8 實戰》一書,裡面有非常非常詳細的介紹。
6 小結
本文簡單介紹了lambda表示式的語法以及使用。lambda表示式確實能大大簡化原本複雜囉嗦的Java程式碼,而且更加靈活,語義也更加清晰明瞭,寫程式碼的時候就好像用自然語言和計算機對話一樣!但也不是哪裡都能使用的,一個最基本的要求就是:其放置的位置要對應著一個函式式介面。函式式介面即只有一個抽象方法的介面,例如Comparator,Runnable等。除此之外,使用lambda表示式的時候,其主體邏輯最好不要超過10行,否則最好還是換一種方式來實現,這裡10行並不是那麼嚴格,具體情況還要具體分析。方法引用是一種特殊情況下的lambda表示式的表示方法,可以理解為是lambda的一個語法糖,其語義更加明確,語法也更加簡潔,用起來還是非常舒服的!
最後,作為一個補充,來簡單看看JDK內建的一些通用性比較強的函式式介面,這些介面都在java.util.function包下,我沒數過,咋一看估計得有40多個吧。常用的有Function,Predicate,Consumer,Supplier等。Function的抽象方法的方法簽名如下所示:
R apply(T t); //T,R是泛型
複製程式碼
簡單從語義上來看,就是傳入一個T型別的值,然後apply函式將其轉換成R型別的值,即一對一對映。其他的介面就不做介紹了。
7 參考資料
《Java8 實戰》