前言
在上一篇文章中踪旷,我們了解了利用行為參數(shù)化來傳遞代碼有助于應(yīng)對不斷變化的需求捻勉,它允許你定義一個代碼塊來表示一個行為,然后傳遞它桥爽。一般來說纳击,利用這個概念续扔,你就可以編寫更為靈活且可重復(fù)使用的代碼了。
但是你同時也看到焕数,使用匿名類來表示不同的行為并不令人滿意:代碼十分啰嗦纱昧,這會影響程序員在時間中使用行為參數(shù)化的積極性。Lambda表達(dá)式很好的解決了這個問題堡赔,它可以讓你很簡潔地表示一個行為或傳遞代碼∈洞啵現(xiàn)在你可以把Lambda表達(dá)式看作匿名功能,它基本上就是沒有聲明名稱的方法善已,但和匿名類一樣灼捂,它也可以作為參數(shù)傳遞給一個方法。
Lambda管中窺豹
可以把Lambda表達(dá)式理解為簡潔地表示可傳遞的匿名函數(shù)的一種方式:它沒有名稱换团,但它由參數(shù)列表悉稠、函數(shù)主體、返回類型艘包,可能還有一個拋出的異常列表的猛。
Lambda表達(dá)式鼓勵你采用上一篇文章中提到的行為參數(shù)化風(fēng)格,最終結(jié)果就是你的額代碼變得更加清晰辑甜、更加靈活衰絮。比如,利用Lambda表達(dá)式磷醋,你可以更為簡潔地自定義一個Comparator對象:
不得不承認(rèn),代碼看起來更清晰了胡诗。要是現(xiàn)在覺得Lambda表達(dá)式看起來一頭霧水的話也沒關(guān)系邓线,很快就會一點(diǎn)點(diǎn)的解釋清楚的。現(xiàn)在煌恢,請注意你基本上只傳遞了比較兩個蘋果重量所需要的代碼骇陈。看起來就像只傳遞了compare方法的主體瑰抵。你很快就會學(xué)到你雌,你甚至還可以進(jìn)一步簡化代碼。
為了進(jìn)一步說明,下面給出了Java 8五個有效的Lambda表達(dá)式的例子:
Java語言設(shè)計者選擇這樣的語法婿崭,是因為C#和Scala等語言中的類似功能廣受歡迎拨拓。Lambda的基本語法是:
(parameters) -> expression
或(請注意語句的花括號)
(parameters) -> { statements; }
你可以看到,Lambda表達(dá)式的語法很簡單氓栈,我們下來來測試一下你對這個模式的了解程度:
在哪里以及如何使用Lambda
現(xiàn)在你可能在想渣磷,在哪里可以使用Lambda表達(dá)式。直接公布答案:你可以在函數(shù)式接口上使用Lambda表達(dá)式授瘦。
函數(shù)式接口
還記得上一篇文章中醋界,為了參數(shù)化filter方法的行為而創(chuàng)建的Predicate<T>接口嗎?它就是一個函數(shù)式接口提完!為什么呢形纺?因為Predicate僅僅定義了一個抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
一言以蔽之,函數(shù)式接口就是之定義一個抽象方法的接口徒欣。你已經(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)方法(即在類沒有對方法進(jìn)行是現(xiàn)實時,其主體為方法提供默認(rèn)實現(xiàn)的方法帚称,如List的sort方法)官研。哪怕有很多默認(rèn)方法,只要接口只定義了一個抽象方法闯睹,它就仍然是一個函數(shù)式接口
為了檢測是否掌握了函數(shù)式接口的概念戏羽,我們來看一個小測試:
用函數(shù)式接口可以干什么呢?Lambda表達(dá)式允許你直接以內(nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實現(xiàn)楼吃,并把整個表達(dá)式作為函數(shù)式接口的實例始花。這聽上去可能有些繞口,但是聯(lián)想到上一篇文章中的Lambda表達(dá)式改造的語句孩锡,或許就會清晰許多酷宵,它不同于使用匿名內(nèi)部類來完成時的笨拙,而是更加清晰直接:
你可能會想:“為什么只有在需要函數(shù)式接口的時候才可以傳遞Lambda呢躬窜?”語言的設(shè)計者也考慮過其他方法浇垦,例如給Java添加函數(shù)類型,但最終他們選擇了現(xiàn)在這種方式荣挨,因為這種方式自然且能避免語言變得更加復(fù)雜男韧。此外,大多數(shù)Java程序員都已經(jīng)熟悉了具有一個抽象方法的接口的理念(例如事件處理)默垄。
第一步:記得行為參數(shù)化
現(xiàn)在這段代碼時有局限的此虑。你只能讀文件的第一行。如果你想要返回頭兩行口锭,甚至返回使用最頻繁的詞朦前,該怎么辦呢?在理想的情況下,你要重用執(zhí)行設(shè)置和清理的代碼韭寸,并告訴processFile方法對文件執(zhí)行不同的操作春哨。這聽起來是不是很耳熟?是的棒仍,你需要把processFile的行為參數(shù)化悲靴。你需要一種方法把行為傳遞給processFile,以便它可以利用BufferedReader執(zhí)行不同的行為莫其。
傳遞行為正是Lambda的拿手好戲癞尚。那要是想一次讀兩行,這個新的processFile方法看起來又該是什么樣的呢乱陡?基本上浇揩,你需要一個接受BufferedReader并返回String的Lambda。例如憨颠,下面就是從BufferedReader中打印兩行的寫法:
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
第二步:使用函數(shù)式接口來傳遞行為
前面已經(jīng)解釋過了胳徽,Lambda僅可用于上下文是函數(shù)式接口的情況。你需要創(chuàng)建一個能匹配BufferedReader -> String爽彤,還可以拋出IOException異常的接口养盗。讓我們把這一接口叫做BufferedReaderProcessor吧。
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
@FunctionalInterface 標(biāo)注表示該接口會設(shè)計成一個函數(shù)式接口适篙。如果你用此標(biāo)注定義了一個接口往核,而它卻不是函數(shù)式接口的話,編譯器將返回一個提示原因的錯誤嚷节。
現(xiàn)在你就可以把這個接口作為新的processFile方法的參數(shù)了:
public static String processFile(BufferedReaderProcessor p) throws IOException{
...
}
第三步:執(zhí)行一個行為
任何BufferedRader -> String形式的Lambda都可以作為參數(shù)來傳遞聂儒,因為它們符合BufferedReaderProcessor接口中定義的process方法的簽名。現(xiàn)在你只需要一種方法在processFile主體內(nèi)執(zhí)行Lambda所代表的代碼硫痰。請記住衩婚,Lambda表達(dá)式允許你直接內(nèi)聯(lián),為函數(shù)式接口的抽象方法提供實現(xiàn)效斑,并且將整個表達(dá)式作為函數(shù)式接口的一個實例非春。因此,你可以在processFile主體內(nèi)鳍悠,對得到的BufferedReaderProcessor對象調(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)在你就可以通過傳遞不同的Lambda重用processFile方法税娜,并以不同的方式處理文件了:
下面的圖片總結(jié)了所采取的使processFile方法更加靈活的四個步驟:
使用函數(shù)式接口
如你所見的,函數(shù)式接口很有用藏研,因為抽象方法的簽名可以描述Lambda表達(dá)式的簽名。Java 8的庫設(shè)計師幫你在java.util.function包中引入了幾個新的函數(shù)式接口概行。
Predicate
java.util.function.Predicate<T>接口定義了一個名叫test的抽象方法蠢挡,它接受泛型T對象,并返回一個boolean。在你需要一個涉及類型T的布爾表達(dá)式時业踏,就可以使用這個接口:
Consumer
java.util.function.Consumer<T>定義了一個名叫accept的抽象方法禽炬,它接受泛型T的對象,沒有返回(void)勤家。你如果需要訪問類型T的對象腹尖,并對其執(zhí)行某些操作,就可以使用這個接口:
Function
java.util.function.Function<T,R>接口定義了一個叫做apply的方法伐脖,它接受一個泛型T的對象热幔,并返回一個泛型R的對象。如果你需要定義一個Lambda讼庇,將輸入對象的信息映射到輸出绎巨,就可以使用這個接口(比如提取蘋果的重量,或把字符串映射為它的長度):
還有更為豐富的一些函數(shù)式接口蠕啄,這里列舉了三個比較有代表性的场勤。
方法引用
方法引用讓你可以重復(fù)使用現(xiàn)有的方法定義,并像Lambda一樣傳遞它們歼跟。在一些情況下和媳,比起使用Lambda表達(dá)式,它們似乎更易讀哈街,感覺也更自然留瞳。下面就是借助Java 8API,用方法引用寫的一個排序的例子:
是不是更酷了叹卷?念起來就是“給庫存排序撼港,比較蘋果的重量”,這樣的代碼讀起來簡直就像是在描述問題本身骤竹,太酷了帝牡。
為什么要關(guān)心方法引用呢?方法引用可以被看作調(diào)用特定方法的Lambda的一種快捷寫法蒙揣。它的基本思想是靶溜,如果一個Lambda代表的知識“直接調(diào)用這個方法”,拿最好還是用名稱來調(diào)用它懒震,而不是去描述如何調(diào)用它罩息。
事實上,方法引用就是讓你根據(jù)已有的方法實現(xiàn)來創(chuàng)建Lambda表達(dá)式个扰,但是瓷炮,顯式地指明方法的名稱,你的代碼可讀性會更好递宅。
它是如何工作的呢娘香?當(dāng)你需要使用方法引用時苍狰,目標(biāo)引用放在分隔符** :: **前,方法的名稱放在后面烘绽。例如淋昭,Apple::getWeight就是引用了Apple類中定義的方法getWeight。請記住安接,不需要括號翔忽,因為你沒有實際調(diào)用這個方法,方法引用就是Lambda表達(dá)式(Apple a) -> a.getWeight()的快捷寫法盏檐。
下面給出一些在Java 8中方法引用的例子來讓你更加了解:
你可以把方法引用看作針對僅僅涉及單一方法的Lambda的語法糖歇式,因為你表達(dá)同樣的事情時寫的代碼更少了。
Lambda 和方法引用實戰(zhàn)
我們繼續(xù)來研究開始的那個問題——用不同的排序策略給一個Apple列表排序糯笙,并展示如何把一個原始粗暴的解決方案轉(zhuǎn)變得更為簡明:inventory.sort(comparing(Apple::getWeight));
第一步:傳遞代碼
很幸運(yùn)贬丛,Java 8的API已經(jīng)為你提供了一個List可用的sort方法,你不用自己去實現(xiàn)它给涕。那么最困難的部分已經(jīng)搞定了豺憔!但是,如何把排序的策略傳遞給sort方法呢够庙?你看恭应,sort方法的簽名是這樣的:
void sort(Comparator<? super E> c)
它需要一個Comparator對象來比較兩個Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個對象里耘眨。我們說sort的行為被參數(shù)化了:傳遞給它的排序策略不同昼榛,其行為也會不同。
你的第一個解決方案看上去是這樣的:
void sort(Comparator<? super E> c)
它需要一個Comparator對象來比較兩個Apple剔难!這就是在Java中傳遞策略的方式:它們必須包裹在一個對象里胆屿。我們說sort的行為被參數(shù)化了:傳遞給它的排序策略不同,其行為也會不同偶宫。
你的第一個解決方案看上去是這樣的:
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1, Apple a2){
return a1.getWeigh().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
第二步:使用匿名類
你可以使用匿名類來改進(jìn)解決方案非迹,而不是實現(xiàn)一個Comparator卻只實例化一次:
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具有一個叫做comparing的靜態(tài)輔助方法憎兽,它可以接受一個Function來提取Comparable鍵值,并生成一個Comparator對象吵冒。它可以像下面這樣用:
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()));
第四步:使用方法引用
前面解釋過纯命,方法引用就是替代那些轉(zhuǎn)發(fā)參數(shù)的Lambda表達(dá)式的語法糖。你可以用方法引用讓你的代碼更加簡潔(假設(shè)你已經(jīng)靜態(tài)導(dǎo)入了java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));
恭喜你痹栖,這就是你的最終解決方案亿汞!這筆Java 8之前的代碼好在哪兒呢?它比較短揪阿;它的意思也很明顯留夜,并且代碼讀起來和問題描述差不多:“對庫存進(jìn)行排序匙铡,比較蘋果的重量图甜“啵”
個人GitHub項目,記錄學(xué)習(xí)Java知識的過程 歡迎star