1 概述
Java8據(jù)說是Java誕生以來最大的一次演進(jìn)酝掩,說實(shí)話问潭,對我個(gè)人來說沒有什么特別大的感受,因?yàn)槲覍W(xué)Java也就最近一兩年的事干跛,Java8在2014年3月18日發(fā)布,新增的特性確實(shí)非常驚艷祟绊,在語言特性層面上新增了lambda楼入,Optional,默認(rèn)方法牧抽,Stream API等嘉熊,在虛擬機(jī)層面上新增了G1收集器(不過在Java9之后才改為默認(rèn)的垃圾收集器)......
我個(gè)人認(rèn)為Java8和語言相關(guān)的幾個(gè)最重要的特性是如下幾個(gè):
- lambda表達(dá)式和方法引用(其實(shí)是lambda表達(dá)式的一種特例)
- Stream API
- 接口的默認(rèn)方法
- Optinal
- CompletableFuture
本系列文章的后面幾篇文章會圍繞這幾個(gè)主題來展開,今天就先上個(gè)開胃菜扬舒,lambda表達(dá)式阐肤!
2 什么是lambda表達(dá)式
lambda表達(dá)式也叫做匿名函數(shù),其基于著名的λ演算得名,關(guān)于λ演算孕惜,推薦大家去找找關(guān)于“丘奇數(shù)”相關(guān)的資料愧薛。Java一直被人詬病的一點(diǎn)就是“啰嗦”,通常為了實(shí)現(xiàn)一個(gè)小功能衫画,就不得不編寫大量的代碼毫炉,而用其他的語言例如Python等,也許寥寥幾行代碼就解決了削罩,但支持lambda表達(dá)式之后碘箍,這一情況得到了大大的改善,現(xiàn)在只要使用得當(dāng)鲸郊,可以大大縮減代碼里丰榴,使代碼的目的更加清晰,易讀秆撮,純粹四濒。
在Java中,很多時(shí)候在使用一些API的時(shí)候职辨,必須要給出一些接口的實(shí)現(xiàn)盗蟆,但因?yàn)樵搶?shí)現(xiàn)其實(shí)也就用一次,專門去創(chuàng)建一個(gè)新的實(shí)現(xiàn)類并不劃算舒裤,所以一般大多數(shù)人采取的措施應(yīng)該是創(chuàng)建一個(gè)匿名實(shí)現(xiàn)類喳资,比較典型就是Collections.sort(List<T> list, Comparator<? super T> c)方法,該方法接受一個(gè)Comparator類型的參數(shù)腾供,Comparator是一個(gè)接口仆邓,表示“比較器”,如果要使用該方法對集合元素進(jìn)行排序伴鳖,就必須提供一個(gè)Comparator接口的實(shí)現(xiàn)节值,否則無法通過編譯。如下所示:
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
其實(shí)這個(gè)實(shí)現(xiàn)類的核心只有一行榜聂,即return o1.compareTo(o2);但我們卻不得不編寫其他“啰嗦”的代碼搞疗,如果使用lambda表達(dá)式,會是怎么個(gè)樣子呢须肆?
Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));
沒錯(cuò)匿乃,就是那么簡單粗暴,就是一行核心代碼豌汇。其他的比如方法簽名啥的統(tǒng)統(tǒng)可以省略了幢炸,不僅簡潔,而且語義也更加清晰瘤礁,讀起來就好像是說:“sort方法阳懂,幫我吧numbers這個(gè)序列排個(gè)序梅尤,排序規(guī)則就按照n1.compareTo(n2)的返回值來決定”」袼迹現(xiàn)在岩调,是不是感覺,寫代碼就像在和計(jì)算機(jī)對話一樣簡單赡盘?但(n1, n2) -> n1.compareTo(n2)這玩意是個(gè)什么鬼号枕?還帶個(gè)箭頭?不用著急陨享,下面馬上介紹lambda表達(dá)式的語法葱淳。
2.1 lambda表達(dá)式的語法
- 第一部分是lambda的參數(shù)列表,因?yàn)镃omparator.compare()方法接受兩個(gè)參數(shù)抛姑,所以這里給出兩個(gè)參數(shù)n1和n2赞厕,可以省略具體的類型,Java編譯器會自動推斷定硝。
- 第二部分是箭頭皿桑,沒什么特殊的地方,只是Java語言覺得使用這個(gè)蔬啡,各個(gè)語言的實(shí)現(xiàn)也不太一樣诲侮,例如Python是:號,簡單理解的就當(dāng)是把參數(shù)列表和函數(shù)主體分開的東西吧箱蟆。
- 第三部分就是函數(shù)主體沟绪,也就是真正執(zhí)行邏輯的地方。
如果函數(shù)主體僅僅包含一行代碼空猜,可以省略花括號{}和return關(guān)鍵字(如果有的話)绽慈。對于我們的例子,可以改寫成這樣:
Collections.sort(numbers, (n1, n2) -> {return n1.compareTo(n2);});
注意分號辈毯!因?yàn)榇藭r(shí)return n1.compareTo(n2);就是一條普通的Java語句了久信,必須遵守Java的語法規(guī)則。好了漓摩,盡管我們現(xiàn)在明白了lambda語句的語法規(guī)則裙士,但還有一個(gè)關(guān)鍵的問題,就是為什么要這樣寫管毙,換句話說腿椎,為什么要有倆參數(shù),這return又是幾個(gè)意思夭咬?還有到底哪里才可以使用lambda表達(dá)式啃炸?說到這,就不得不說一下和lambda息息相關(guān)的東西了:函數(shù)式接口卓舵。
3 函數(shù)式接口
函數(shù)式接口是這樣的:只有一個(gè)抽象方法的接口就是函數(shù)式接口南用。為什么要特別強(qiáng)調(diào)抽象方法呢?Java接口里聲明的方法不都是抽象方法嗎?在Java8之前裹虫,這么說確實(shí)沒有任何問題肿嘲,但Java8新增了接口的默認(rèn)方法,可以在接口里給出方法的具體實(shí)現(xiàn)筑公,這里先不多說雳窟,后面的文章會詳細(xì)討論這個(gè)東西。
lambda表達(dá)式僅可以用在函數(shù)式接口上匣屡,我們在上面遇到的Comparator就是一個(gè)函數(shù)式接口封救,他只有一個(gè)抽象方法:compare(),其方法簽名是這樣的:
int compare(T o1, T o2);
現(xiàn)在來看看 (n1, n2) -> n1.compareTo(n2)這個(gè)表達(dá)式捣作,是不是發(fā)現(xiàn)了什么誉结?沒錯(cuò),其實(shí)lambda表達(dá)式的參數(shù)列表就是對應(yīng)的函數(shù)式接口的抽象方法的參數(shù)列表券躁,并且類型可以省略(編譯器自動推斷)搓彻,然后n1.compareTo(n2)的返回值是int類型,也符合compare()的方法描述嘱朽。這樣就算是把lambda表達(dá)式和接口的抽象方法簽名匹配成功了旭贬,不會出現(xiàn)編譯錯(cuò)誤。
除此之外搪泳,Runnable也是一個(gè)函數(shù)式接口稀轨,它只有一個(gè)抽象方法,即run()岸军,run()方法的方法簽名如下所示:
public abstract void run();
不接受任何參數(shù)奋刽,也沒有返回值。那如果要編寫對應(yīng)的lambda表達(dá)式艰赞,該如何做呢佣谐?其實(shí)非常簡單,下面是一個(gè)示例:
Runnable r = () -> {
System.out.println(Thread.currentThread().getName());
//do something
};
如果觀察仔細(xì)的話方妖,會發(fā)現(xiàn)狭魂,示例代碼中把這個(gè)lambda表達(dá)式賦值給了Runnable類型的變量r!經(jīng)過上面的討論党觅,我們知道雌澄,其實(shí)lambda就是一個(gè)方法實(shí)現(xiàn)(其實(shí)叫做函數(shù)會更加合適),這條賦值語句看起來就好像是再說:“把方法(函數(shù))賦值給變量杯瞻!”镐牺。如果沒有接觸過函數(shù)式編程,會覺得這樣很奇怪魁莉,怎么能把方法賦值給變量呢睬涧?計(jì)算機(jī)就是這樣有意思募胃,總是有各種各樣奇奇怪怪的東西沖擊我們的思維!那這有什么用呢畦浓?咱先不說什么高階函數(shù)痹束,科里化啥的(這些是函數(shù)式編程里的概念),就說一點(diǎn):意味著我們可以把方法(函數(shù))當(dāng)做變量來使用宅粥!即現(xiàn)在方法就是Java世界里的“一等公民”了参袱!既可以將其作為參數(shù)傳遞給其他方法(函數(shù))电谣,還可以將其作為其他方法(函數(shù))的返回值(以后會講到具體的案例)
4 策略模式
策略模式是著名的23種設(shè)計(jì)模式中的一種秽梅,關(guān)于它的描述,我這里就不多說了剿牺。直接來看個(gè)例子吧企垦。
例子是這樣的,現(xiàn)在有一個(gè)代表汽車的Car類以及一個(gè)Car列表晒来,現(xiàn)在我們想要篩選列表中符合要求的汽車钞诡,為了應(yīng)對多變的篩選方法,我們打算用策略模式來實(shí)現(xiàn)功能湃崩。
下面是Car類的代碼:
public class Car {
//品牌
private String brand;
//顏色
private Color color;
//車齡
private Integer age;
//三個(gè)參數(shù)的構(gòu)造函數(shù)以及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", Car.Color.BLACK,1),
new Car("Audi", Car.Color.PINK, 5));
我們希望用一個(gè)方法來封裝篩選的邏輯荧降,其方法簽名偽代碼如下所示:
cars carFilter(cars, filterStrategy);
接下來實(shí)現(xiàn)策略模式,下面是相關(guān)的代碼:
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());
}
}
為了簡單攒读,僅僅實(shí)現(xiàn)了兩種篩選策略朵诫,第一種是刪選出品牌是“BWM”的汽車,第二種是刪選出顏色為紅色的汽車薄扁。最后來實(shí)現(xiàn)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()));
}
分別實(shí)例化兩個(gè)策略,將其作為參數(shù)傳遞給carFilter()方法邓梅,最終的輸出如下所示:
[Car{brand='BWM', color=BLACK, age=2}]
----------------------------------------
[Car{brand='BENZ', color=RED, age=3}]
確實(shí)符合預(yù)期脱盲。是不是就到此為止了呢?當(dāng)然不日缨!我們發(fā)現(xiàn)钱反,其實(shí)BWMCarFilterStrategy以及RedColorCarFilterStrategy的實(shí)現(xiàn)代碼都非常簡單,僅僅寥寥幾行代碼匣距,而且CarFilterStrategy接口僅僅有一個(gè)filter抽象方法诈铛,顯然是一個(gè)函數(shù)式接口,那我們能不能用lambda表達(dá)式來簡化呢墨礁?答案是:完全可以幢竹!而且更加推薦用lambda表達(dá)式來簡化這種情況。
4.1 用lambda表達(dá)式來簡化代碼
只要略微做一些修改就行了:
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兩個(gè)類了恩静,直接用lambda表達(dá)式就行了焕毫!最后把這倆實(shí)現(xiàn)刪除掉蹲坷!是不是頓時(shí)感覺整個(gè)項(xiàng)目的代碼清爽了許多?
4.2 需要注意的
其實(shí)本小節(jié)的例子有些過于特殊了邑飒,如果你項(xiàng)目中的策略模式的實(shí)現(xiàn)非常復(fù)雜循签,其策略不是簡簡單單的幾行代碼就能解決的,此時(shí)要么進(jìn)一步封裝代碼疙咸,要么就最好不要用lambda表達(dá)式了县匠,因?yàn)槿绻壿嫃?fù)雜的話,強(qiáng)行使用lambda不僅僅不能簡化代碼撒轮,反而會使得代碼更加晦澀乞旦。
5 方法引用
最后簡單講一下方法引用吧,方法引用其實(shí)是lambda表達(dá)式的一種特殊情況的表示题山,語法規(guī)則是:
<class name or instance name>:<method name>
如果lambda表達(dá)式的主體邏輯僅僅是一個(gè)調(diào)用方法的語句的話兰粉,那么就可以將其轉(zhuǎn)換為方法引用,如下所示:
//普通的lambda表達(dá)式
numbers.forEach(n -> System.out.println(n));
//轉(zhuǎn)換成方法引用
numbers.forEach(System.out::println);
他倆效果是完全一樣的顶瞳,但顯然方法引用更加簡潔玖姑,語義也更加明確了,這一語法糖“真香慨菱!”焰络。具體的我就不多說了,建議看看《Java8 實(shí)戰(zhàn)》一書符喝,里面有非常非常詳細(xì)的介紹闪彼。
6 小結(jié)
本文簡單介紹了lambda表達(dá)式的語法以及使用。lambda表達(dá)式確實(shí)能大大簡化原本復(fù)雜啰嗦的Java代碼洲劣,而且更加靈活备蚓,語義也更加清晰明了,寫代碼的時(shí)候就好像用自然語言和計(jì)算機(jī)對話一樣囱稽!但也不是哪里都能使用的郊尝,一個(gè)最基本的要求就是:其放置的位置要對應(yīng)著一個(gè)函數(shù)式接口。函數(shù)式接口即只有一個(gè)抽象方法的接口战惊,例如Comparator流昏,Runnable等。除此之外吞获,使用lambda表達(dá)式的時(shí)候况凉,其主體邏輯最好不要超過10行,否則最好還是換一種方式來實(shí)現(xiàn)各拷,這里10行并不是那么嚴(yán)格刁绒,具體情況還要具體分析。方法引用是一種特殊情況下的lambda表達(dá)式的表示方法烤黍,可以理解為是lambda的一個(gè)語法糖知市,其語義更加明確傻盟,語法也更加簡潔,用起來還是非常舒服的嫂丙!
最后娘赴,作為一個(gè)補(bǔ)充,來簡單看看JDK內(nèi)置的一些通用性比較強(qiáng)的函數(shù)式接口跟啤,這些接口都在java.util.function包下诽表,我沒數(shù)過,咋一看估計(jì)得有40多個(gè)吧隅肥。常用的有Function竿奏,Predicate,Consumer武福,Supplier等议双。Function的抽象方法的方法簽名如下所示:
R apply(T t); //T,R是泛型
簡單從語義上來看痘番,就是傳入一個(gè)T類型的值捉片,然后apply函數(shù)將其轉(zhuǎn)換成R類型的值,即一對一映射汞舱。其他的接口就不做介紹了伍纫。
7 參考資料
《Java8 實(shí)戰(zhàn)》