Java 8——Lambda表達(dá)式

本文內(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,128評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,291評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,421評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,642評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,821評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,053評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容