本文內(nèi)容大部分來(lái)自《Java 8實(shí)戰(zhàn)》一書(shū)
前言
在上一篇文章中,我們了解了利用行為參數(shù)化來(lái)傳遞代碼有助于應(yīng)對(duì)不斷變化的需求,它允許你定義一個(gè)代碼塊來(lái)表示一個(gè)行為,然后傳遞它。一般來(lái)說(shuō),利用這個(gè)概念,你就可以編寫(xiě)更為靈活且可重復(fù)使用的代碼了。
但是你同時(shí)也看到,使用匿名類(lèi)來(lái)表示不同的行為并不令人滿意:代碼十分啰嗦,這會(huì)影響程序員在時(shí)間中使用行為參數(shù)化的積極性。Lambda表達(dá)式很好的解決了這個(gè)問(wèn)題,它可以讓你很簡(jiǎn)潔地表示一個(gè)行為或傳遞代碼。現(xiàn)在你可以把Lambda表達(dá)式看作匿名功能,它基本上就是沒(méi)有聲明名稱(chēng)的方法,但和匿名類(lèi)一樣,它也可以作為參數(shù)傳遞給一個(gè)方法。
Lambda管中窺豹
可以把Lambda表達(dá)式理解為簡(jiǎn)潔地表示可傳遞的匿名函數(shù)的一種方式:它沒(méi)有名稱(chēng),但它由參數(shù)列表、函數(shù)主體、返回類(lèi)型,可能還有一個(gè)拋出的異常列表。
Lambda表達(dá)式鼓勵(lì)你采用上一篇文章中提到的行為參數(shù)化風(fēng)格,最終結(jié)果就是你的額代碼變得更加清晰、更加靈活。比如,利用Lambda表達(dá)式,你可以更為簡(jiǎn)潔地自定義一個(gè)Comparator對(duì)象:
不得不承認(rèn),代碼看起來(lái)更清晰了。要是現(xiàn)在覺(jué)得Lambda表達(dá)式看起來(lái)一頭霧水的話也沒(méi)關(guān)系,很快就會(huì)一點(diǎn)點(diǎn)的解釋清楚的。現(xiàn)在,請(qǐng)注意你基本上只傳遞了比較兩個(gè)蘋(píng)果重量所需要的代碼。看起來(lái)就像只傳遞了compare方法的主體。你很快就會(huì)學(xué)到,你甚至還可以進(jìn)一步簡(jiǎn)化代碼。
為了進(jìn)一步說(shuō)明,下面給出了Java 8五個(gè)有效的Lambda表達(dá)式的例子:
Java語(yǔ)言設(shè)計(jì)者選擇這樣的語(yǔ)法,是因?yàn)镃#和Scala等語(yǔ)言中的類(lèi)似功能廣受歡迎。Lambda的基本語(yǔ)法是:
(parameters) -> expression
或(請(qǐng)注意語(yǔ)句的花括號(hào))
(parameters) -> { statements; }
你可以看到,Lambda表達(dá)式的語(yǔ)法很簡(jiǎn)單,我們下來(lái)來(lái)測(cè)試一下你對(duì)這個(gè)模式的了解程度:
在哪里以及如何使用Lambda
現(xiàn)在你可能在想,在哪里可以使用Lambda表達(dá)式。直接公布答案:你可以在函數(shù)式接口上使用Lambda表達(dá)式。
函數(shù)式接口
還記得上一篇文章中,為了參數(shù)化filter方法的行為而創(chuàng)建的Predicate<T>接口嗎?它就是一個(gè)函數(shù)式接口!為什么呢?因?yàn)镻redicate僅僅定義了一個(gè)抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
一言以蔽之,函數(shù)式接口就是之定義一個(gè)抽象方法的接口。你已經(jīng)知道了Java API中的一些其他函數(shù)式接口,如Comparator和Runnable
public interface Comparator<T>{
int compare(T o1, T o2);
}
public interface Runnable{
void run();
}
接口現(xiàn)在還可以擁有默認(rèn)方法(即在類(lèi)沒(méi)有對(duì)方法進(jìn)行是現(xiàn)實(shí)時(shí),其主體為方法提供默認(rèn)實(shí)現(xiàn)的方法,如List的sort方法)。哪怕有很多默認(rèn)方法,只要接口只定義了一個(gè)抽象方法,它就仍然是一個(gè)函數(shù)式接口。
為了檢測(cè)是否掌握了函數(shù)式接口的概念,我們來(lái)看一個(gè)小測(cè)試:
用函數(shù)式接口可以干什么呢?Lambda表達(dá)式允許你直接以內(nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實(shí)現(xiàn),并把整個(gè)表達(dá)式作為函數(shù)式接口的實(shí)例。這聽(tīng)上去可能有些繞口,但是聯(lián)想到上一篇文章中的Lambda表達(dá)式改造的語(yǔ)句,或許就會(huì)清晰許多,它不同于使用匿名內(nèi)部類(lèi)來(lái)完成時(shí)的笨拙,而是更加清晰直接:
你可能會(huì)想:“為什么只有在需要函數(shù)式接口的時(shí)候才可以傳遞Lambda呢?”語(yǔ)言的設(shè)計(jì)者也考慮過(guò)其他方法,例如給Java添加函數(shù)類(lèi)型,但最終他們選擇了現(xiàn)在這種方式,因?yàn)檫@種方式自然且能避免語(yǔ)言變得更加復(fù)雜。此外,大多數(shù)Java程序員都已經(jīng)熟悉了具有一個(gè)抽象方法的接口的理念(例如事件處理)。
把Lambda付諸實(shí)踐:環(huán)繞執(zhí)行模式
讓我們通過(guò)一個(gè)例子,看看在實(shí)踐中如何利用Lambda和行為參數(shù)化來(lái)讓代碼更為靈活,更為簡(jiǎn)潔。資源處理(例如處理文件或數(shù)據(jù)庫(kù))時(shí)一個(gè)常見(jiàn)的模式就是打開(kāi)一個(gè)資源,做一些處理,然后關(guān)閉資源。這個(gè)設(shè)置和清理階段總是很相似,并且會(huì)圍繞著執(zhí)行處理的那些重要代碼。這就是所謂的環(huán)繞執(zhí)行(execute around)模式:
第一步:記得行為參數(shù)化
現(xiàn)在這段代碼時(shí)有局限的。你只能讀文件的第一行。如果你想要返回頭兩行,甚至返回使用最頻繁的詞,該怎么辦呢?在理想的情況下,你要重用執(zhí)行設(shè)置和清理的代碼,并告訴processFile方法對(duì)文件執(zhí)行不同的操作。這聽(tīng)起來(lái)是不是很耳熟?是的,你需要把processFile的行為參數(shù)化。你需要一種方法把行為傳遞給processFile,以便它可以利用BufferedReader執(zhí)行不同的行為。
傳遞行為正是Lambda的拿手好戲。那要是想一次讀兩行,這個(gè)新的processFile方法看起來(lái)又該是什么樣的呢?基本上,你需要一個(gè)接受BufferedReader并返回String的Lambda。例如,下面就是從BufferedReader中打印兩行的寫(xiě)法:
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
第二步:使用函數(shù)式接口來(lái)傳遞行為
前面已經(jīng)解釋過(guò)了,Lambda僅可用于上下文是函數(shù)式接口的情況。你需要?jiǎng)?chuàng)建一個(gè)能匹配BufferedReader -> String,還可以拋出IOException異常的接口。讓我們把這一接口叫做BufferedReaderProcessor吧。
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
@FunctionalInterface 標(biāo)注表示該接口會(huì)設(shè)計(jì)成一個(gè)函數(shù)式接口。如果你用此標(biāo)注定義了一個(gè)接口,而它卻不是函數(shù)式接口的話,編譯器將返回一個(gè)提示原因的錯(cuò)誤。
現(xiàn)在你就可以把這個(gè)接口作為新的processFile方法的參數(shù)了:
public static String processFile(BufferedReaderProcessor p) throws IOException{
...
}
第三步:執(zhí)行一個(gè)行為
任何BufferedRader -> String形式的Lambda都可以作為參數(shù)來(lái)傳遞,因?yàn)樗鼈兎螧ufferedReaderProcessor接口中定義的process方法的簽名。現(xiàn)在你只需要一種方法在processFile主體內(nèi)執(zhí)行Lambda所代表的代碼。請(qǐng)記住,Lambda表達(dá)式允許你直接內(nèi)聯(lián),為函數(shù)式接口的抽象方法提供實(shí)現(xiàn),并且將整個(gè)表達(dá)式作為函數(shù)式接口的一個(gè)實(shí)例。因此,你可以在processFile主體內(nèi),對(duì)得到的BufferedReaderProcessor對(duì)象調(diào)用process方法執(zhí)行處理:
public static String processFile(BufferedReaderProcesssor p) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}
第四步:傳遞Lambda
現(xiàn)在你就可以通過(guò)傳遞不同的Lambda重用processFile方法,并以不同的方式處理文件了:
下面的圖片總結(jié)了所采取的使processFile方法更加靈活的四個(gè)步驟:
使用函數(shù)式接口
如你所見(jiàn)的,函數(shù)式接口很有用,因?yàn)槌橄蠓椒ǖ暮灻梢悦枋鯨ambda表達(dá)式的簽名。Java 8的庫(kù)設(shè)計(jì)師幫你在java.util.function包中引入了幾個(gè)新的函數(shù)式接口。
Predicate
java.util.function.Predicate<T>接口定義了一個(gè)名叫test的抽象方法,它接受泛型T對(duì)象,并返回一個(gè)boolean。在你需要一個(gè)涉及類(lèi)型T的布爾表達(dá)式時(shí),就可以使用這個(gè)接口:
Consumer
java.util.function.Consumer<T>定義了一個(gè)名叫accept的抽象方法,它接受泛型T的對(duì)象,沒(méi)有返回(void)。你如果需要訪問(wèn)類(lèi)型T的對(duì)象,并對(duì)其執(zhí)行某些操作,就可以使用這個(gè)接口:
Function
java.util.function.Function<T,R>接口定義了一個(gè)叫做apply的方法,它接受一個(gè)泛型T的對(duì)象,并返回一個(gè)泛型R的對(duì)象。如果你需要定義一個(gè)Lambda,將輸入對(duì)象的信息映射到輸出,就可以使用這個(gè)接口(比如提取蘋(píng)果的重量,或把字符串映射為它的長(zhǎng)度):
還有更為豐富的一些函數(shù)式接口,這里列舉了三個(gè)比較有代表性的。
方法引用
方法引用讓你可以重復(fù)使用現(xiàn)有的方法定義,并像Lambda一樣傳遞它們。在一些情況下,比起使用Lambda表達(dá)式,它們似乎更易讀,感覺(jué)也更自然。下面就是借助Java 8API,用方法引用寫(xiě)的一個(gè)排序的例子:
是不是更酷了?念起來(lái)就是“給庫(kù)存排序,比較蘋(píng)果的重量”,這樣的代碼讀起來(lái)簡(jiǎn)直就像是在描述問(wèn)題本身,太酷了。
為什么要關(guān)心方法引用呢?方法引用可以被看作調(diào)用特定方法的Lambda的一種快捷寫(xiě)法。它的基本思想是,如果一個(gè)Lambda代表的知識(shí)“直接調(diào)用這個(gè)方法”,拿最好還是用名稱(chēng)來(lái)調(diào)用它,而不是去描述如何調(diào)用它。
事實(shí)上,方法引用就是讓你根據(jù)已有的方法實(shí)現(xiàn)來(lái)創(chuàng)建Lambda表達(dá)式,但是,顯式地指明方法的名稱(chēng),你的代碼可讀性會(huì)更好。
它是如何工作的呢?當(dāng)你需要使用方法引用時(shí),目標(biāo)引用放在分隔符** :: **前,方法的名稱(chēng)放在后面。例如,Apple::getWeight就是引用了Apple類(lèi)中定義的方法getWeight。請(qǐng)記住,不需要括號(hào),因?yàn)槟銢](méi)有實(shí)際調(diào)用這個(gè)方法,方法引用就是Lambda表達(dá)式(Apple a) -> a.getWeight()的快捷寫(xiě)法。
下面給出一些在Java 8中方法引用的例子來(lái)讓你更加了解:
你可以把方法引用看作針對(duì)僅僅涉及單一方法的Lambda的語(yǔ)法糖,因?yàn)槟惚磉_(dá)同樣的事情時(shí)寫(xiě)的代碼更少了。
Lambda 和方法引用實(shí)戰(zhàn)
我們繼續(xù)來(lái)研究開(kāi)始的那個(gè)問(wèn)題——用不同的排序策略給一個(gè)Apple列表排序,并展示如何把一個(gè)原始粗暴的解決方案轉(zhuǎn)變得更為簡(jiǎn)明:inventory.sort(comparing(Apple::getWeight));
第一步:傳遞代碼
很幸運(yùn),Java 8的API已經(jīng)為你提供了一個(gè)List可用的sort方法,你不用自己去實(shí)現(xiàn)它。那么最困難的部分已經(jīng)搞定了!但是,如何把排序的策略傳遞給sort方法呢?你看,sort方法的簽名是這樣的:
void sort(Comparator<? super E> c)
它需要一個(gè)Comparator對(duì)象來(lái)比較兩個(gè)Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個(gè)對(duì)象里。我們說(shuō)sort的行為被參數(shù)化了:傳遞給它的排序策略不同,其行為也會(huì)不同。
你的第一個(gè)解決方案看上去是這樣的:
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1, Apple a2){
return a1.getWeigh().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
第二步:使用匿名類(lèi)
你可以使用匿名類(lèi)來(lái)改進(jìn)解決方案,而不是實(shí)現(xiàn)一個(gè)Comparator卻只實(shí)例化一次:
inventory.sort(new Comparator<Apple>(){
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
第三步:使用Lambda表達(dá)式
但你的解決方案仍然挺啰嗦的。使用Java 8引入的Lambda改進(jìn)后的代碼如下:
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
你的代碼還能變得更易讀一點(diǎn)嗎?Comparator具有一個(gè)叫做comparing的靜態(tài)輔助方法,它可以接受一個(gè)Function來(lái)提取Comparable鍵值,并生成一個(gè)Comparator對(duì)象。它可以像下面這樣用:
Comparator<Apple> c = Comparator.comparing((Apple a1) -> a.getWeight());
現(xiàn)在你可以把代碼再改得緊湊一點(diǎn)了:
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
第四步:使用方法引用
前面解釋過(guò),方法引用就是替代那些轉(zhuǎn)發(fā)參數(shù)的Lambda表達(dá)式的語(yǔ)法糖。你可以用方法引用讓你的代碼更加簡(jiǎn)潔(假設(shè)你已經(jīng)靜態(tài)導(dǎo)入了java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));
恭喜你,這就是你的最終解決方案!這筆Java 8之前的代碼好在哪兒呢?它比較短;它的意思也很明顯,并且代碼讀起來(lái)和問(wèn)題描述差不多:“對(duì)庫(kù)存進(jìn)行排序,比較蘋(píng)果的重量。”
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處!
簡(jiǎn)書(shū)ID:@我沒(méi)有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加qq群:3382693