- 詳解Java中的Lambda表達式 推薦度:
- 相關(guān)推薦
java中lambda表達式
第一章 你好,lambda表達式!
第一節
Java的編碼風(fēng)格正面臨著(zhù)翻天覆地的變化。
我們每天的工作將會(huì )變成更簡(jiǎn)單方便,更富表現力。Java這種新的編程方式早在數十年前就已經(jīng)出現在別的編程語(yǔ)言里面了。這些新特性引入Java后,我們可以寫(xiě)出更簡(jiǎn)潔,優(yōu)雅,表達性更強,錯誤更少的代碼。我們可以用更少的代碼來(lái)實(shí)現各種策略和設計模式。
在本書(shū)中我們將通過(guò)日常編程中的一些例子來(lái)探索函數式風(fēng)格的編程。在使用這種全新的優(yōu)雅的方式進(jìn)行設計編碼之前,我們先來(lái)看下它到底好在哪里。
改變了你的思考方式
命令式風(fēng)格——Java語(yǔ)言從誕生之初就一直提供的是這種方式。使用這種風(fēng)格的話(huà),我們得告訴Java每一步要做什么,然后看著(zhù)它切實(shí)的一步步執行下去。這樣做當然很好,就是顯得有點(diǎn)初級。代碼看起來(lái)有點(diǎn)啰嗦,我們希望這個(gè)語(yǔ)言能變得稍微智能一點(diǎn);我們應該直接告訴它我們想要什么,而不是告訴它如何去做。好在現在Java終于可以幫我們實(shí)現這個(gè)愿望了。我們先來(lái)看幾個(gè)例子,了解下這種風(fēng)格的優(yōu)點(diǎn)和不同之處。
正常的方式
我們先從兩個(gè)熟悉的例子來(lái)開(kāi)始。這是用命令的方式來(lái)查看芝加哥是不是指定的城市集合里——記住,本書(shū)中列出的代碼只是部分片段而已。
復制代碼 代碼如下:
boolean found = false;
for(String city : cities) {
if(city.equals("Chicago")) {
found = true;
break;
}
}
System.out.println("Found chicago" + found);
這個(gè)命令式的版本看起來(lái)有點(diǎn)啰嗦而且初級;它分成好幾個(gè)執行部分。先是初始化一個(gè)叫found的布爾標記,然后遍歷集合里的每一個(gè)元素;如果發(fā)現我們要找的城市了,設置下這個(gè)標記,然后跳出循環(huán)體;最后打印出查找的結果。
一種更好的方式
細心的Java程序員看完這段代碼后,很快會(huì )想到一種更簡(jiǎn)潔明了的方式,就像這樣:
復制代碼 代碼如下:
System.out.println("Found chicago" + cities.contains("Chicago"));
這也是一種命令式風(fēng)格的寫(xiě)法——contains方法直接就幫我們搞定了。
實(shí)際改進(jìn)的地方
代碼這么寫(xiě)有這幾個(gè)好處:
1.不用再搗鼓那個(gè)可變的變量了
2.將迭代封裝到了底層
3.代碼更簡(jiǎn)潔
4.代碼更清晰,更聚焦
5.少走彎路,代碼和業(yè)務(wù)需求結合更密切
6.不易出錯
7.易于理解和維護
來(lái)個(gè)復雜點(diǎn)的例子
這個(gè)例子太簡(jiǎn)單了,命令式查詢(xún)一個(gè)元素是否存在于某個(gè)集合在Java里隨處可見(jiàn),F在假設我們要用命令式編程來(lái)進(jìn)行些更高級的操作,比如解析文件 ,和數據庫交互,調用WEB服務(wù),并發(fā)編程等等,F在我們用Java可以寫(xiě)出更簡(jiǎn)潔優(yōu)雅同時(shí)出錯更少的代碼,更不只是這種簡(jiǎn)單的場(chǎng)景。
老的方式
我們來(lái)看下另一個(gè)例子。我們定義了一系列價(jià)格,并通過(guò)不同的方式來(lái)計算打折后的總價(jià)。
復制代碼 代碼如下:
final Listprices = Arrays.asList(
new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"),
new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"),
new BigDecimal("45"), new BigDecimal("12"));
假設超過(guò)20塊的話(huà)要打九折,我們先用普通的方式實(shí)現一遍。
復制代碼 代碼如下:
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal price : prices) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
這個(gè)代碼很熟悉吧;先用一個(gè)變量來(lái)存儲總價(jià);然后遍歷所有的價(jià)格,找出大于20塊的,算出它們的折扣價(jià),并加到總價(jià)里面;最后打印出折扣后的總價(jià)。
下面是程序的輸出:
復制代碼 代碼如下:
Total of discounted prices: 67.5
結果完全正確,不過(guò)這樣的代碼有點(diǎn)亂。這并不是我們的錯,我們只能用已有的方式來(lái)寫(xiě)。不過(guò)這樣的代碼實(shí)在有點(diǎn)初級,它不僅存在基本類(lèi)型偏執,而且還違反了單一職責原則。如果你是在家工作并且家里還有想當碼農的小孩的話(huà),你可得把你的代碼藏好了,萬(wàn)一他們看見(jiàn)了會(huì )很失望地嘆氣道,“你是靠這些玩意兒糊口的?”
還有更好的方式
我們還能做的更好——并且要好很多。我們的代碼有點(diǎn)像需求規范。這樣能縮小業(yè)務(wù)需求和實(shí)現的代碼之間的差距,減少了需求被誤讀的可能性。
我們不再讓Java去創(chuàng )建一個(gè)變量然后沒(méi)完沒(méi)了的給它賦值了,我們要從一個(gè)更高層次的抽象去與它溝通,就像下面的這段代碼。
復制代碼 代碼如下:
final BigDecimal totalOfDiscountedPrices =
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(price -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
大聲的讀出來(lái)吧——過(guò)濾出大于20塊的價(jià)格,把它們轉化成折扣價(jià),然后加起來(lái)。這段代碼和我們描述需求的流程簡(jiǎn)直一模一樣。Java里還可以很方便的把一行長(cháng)的代碼折疊起來(lái),根據方法名前面的點(diǎn)號進(jìn)行按行對齊,就像上面那樣。
代碼非常簡(jiǎn)潔,不過(guò)我們用到了Java8里面的很多新東西。首先,我們調用 了價(jià)格列表的一個(gè)stream方法。這打開(kāi)了一扇大門(mén),門(mén)后邊有數不盡的便捷的迭代器,這個(gè)我們在后面會(huì )繼續討論。
我們用了一些特殊的方法,比如filter和map,而不是直接的遍歷整個(gè)列表。這些方法不像我們以前用的JDK里面的那些,它們接受一個(gè)匿名的函數——lambda表達式——作為參數。(后面我們會(huì )深入的展開(kāi)討論)。我們調用reduce()方法來(lái)計算map()方法返回的價(jià)格的總和。
就像contains方法那樣,循環(huán)體被隱藏起來(lái)了。不過(guò)map方法(以及filter方法)則更復雜得多 。它對價(jià)格列表中的每一個(gè)價(jià)格,調用了傳進(jìn)來(lái)的lambda表達式進(jìn)行計算,把結果放到一個(gè)新的集合里面。最后我們在這個(gè)新的集合上調用 reduce方法得出最終的結果。
這是以上代碼的輸出結果:
復制代碼 代碼如下:
Total of discounted prices: 67.5
改進(jìn)的地方
這和前面的實(shí)現相比改進(jìn)明顯:
1.結構良好而不混亂
2.沒(méi)有低級操作
3.易于增強或者修改邏輯
4.由方法庫來(lái)進(jìn)行迭代
5.高效;循環(huán)體惰性求值
6.易于并行化
下面我們會(huì )說(shuō)到Java是如何實(shí)現這些的。
lambda表達式來(lái)拯救世界了
lambda表達式是讓我們遠離命令式編程煩惱的快捷鍵。Java提供的這個(gè)新特性,改變了我們原有的編程方式,使得我們寫(xiě)出的代碼不僅簡(jiǎn)潔優(yōu)雅,不易出錯,而且效率更高,易于優(yōu)化改進(jìn)和并行化。
第二節:函數式編程的最大收獲
函數式風(fēng)格的代碼有更高的信噪比;寫(xiě)的代碼更少了,但每一行或者每個(gè)表達式做的卻更多了。比命令式編程相比,函數式編程讓我們獲益良多:
避免了對變量的顯式的修改或賦值,這些通常是BUG的根源,并導致代碼很難并行化。在命令行編程中我們在循環(huán)體內不停的對totalOfDiscountedPrices變量賦值。在函數式風(fēng)格里,代碼不再出現顯式的修改操作。變量修改的越少,代碼的BUG就越少。
函數式風(fēng)格的代碼可以輕松的實(shí)現并行化。如果計算很費時(shí),我們可以很容易讓列表中的元素并發(fā)的執行。如果我們想把命令式的代碼并行化,我們還得擔心并發(fā)修改totalOfDiscountedPrices變量帶來(lái)的問(wèn)題。在函數式編程中我們只會(huì )在完全處理完后才訪(fǎng)問(wèn)這個(gè)變量,這樣就消除了線(xiàn)程安全的隱患。
代碼的表達性更強。命令式編程要分成好幾個(gè)步驟要說(shuō)明要做什么——創(chuàng )建一個(gè)初始化的值,遍歷價(jià)格,把折扣價(jià)加到變量上等等——而函數式的話(huà)只需要讓列表的map方法返回一個(gè)包括折扣價(jià)的新的列表然后進(jìn)行累加就可以了。
函數式編程更簡(jiǎn)潔;和命令式相比同樣的結果只需要更少的代碼就能完成。代碼更簡(jiǎn)潔意味著(zhù)寫(xiě)的代碼少了,讀的也少了,維護的也少了——看下第7頁(yè)的"簡(jiǎn)潔少就是簡(jiǎn)潔了嗎"。
函數式的代碼更直觀(guān)——讀代碼就像描述問(wèn)題一樣——一旦我們熟悉語(yǔ)法后就很容易能看懂。map方法對集合的每個(gè)元素都執行了一遍給定的函數(計算折扣價(jià)),然后返回結果集,就像下圖演示的這樣。
圖1——map對集合中的每個(gè)元素執行給定的函數
有了lambda表達式之后,我們可以在Java里充分發(fā)揮函數式編程的威力。使用函數式風(fēng)格,就能寫(xiě)出表達性更佳,更簡(jiǎn)潔,賦值操作更少,錯誤更少的代碼了。
支持面向對象編程是Java一個(gè)主要的優(yōu)點(diǎn)。函數式編程和面向對象編程并不排斥。真正的風(fēng)格變化是從命令行編程轉到聲明式編程。在Java 8里,函數式和面向對象可以有效的融合到一起。我們可以繼續用OOP的風(fēng)格來(lái)對領(lǐng)域實(shí)體以及它們的狀態(tài),關(guān)系進(jìn)行建模。除此之外,我們還可以對行為或者狀態(tài)的轉變,工作流和數據處理用函數來(lái)進(jìn)行建模,建立復合函數。
第三節:為什么要用函數式風(fēng)格?
我們看到了函數式編程的各項優(yōu)點(diǎn),不過(guò)使用這種新的風(fēng)格劃得來(lái)嗎?這只是個(gè)小改進(jìn)還是說(shuō)換頭換面?在真正在這上面花費工夫前,還有很多現實(shí)的問(wèn)題需要解答。
復制代碼 代碼如下:
小明問(wèn)到:
代碼少就是簡(jiǎn)潔了嗎?
簡(jiǎn)潔是少而不亂,歸根結底是說(shuō)要能有效的表達意圖。它帶來(lái)的好處意義深遠。
寫(xiě)代碼就好像把配料堆到一起,簡(jiǎn)潔就是說(shuō)能把配料調成調料。要寫(xiě)出簡(jiǎn)潔的代碼可得下得狠工夫。讀的代碼是少了,真正有用的代碼對你是透明的。一段很難理解或者隱藏細節的短代碼只能說(shuō)是簡(jiǎn)短而不是簡(jiǎn)潔。
簡(jiǎn)潔的代碼竟味著(zhù)敏捷的設計。簡(jiǎn)潔的代碼少了那些繁文縟節。這是說(shuō)我們可以對想法進(jìn)行快速?lài)L試,如果不錯就繼續,如果效果不佳就迅速跳過(guò)。
用Java寫(xiě)代碼并不難,語(yǔ)法簡(jiǎn)單。而且我們也已經(jīng)對現有的庫和API很了如指掌了。真正難的是要拿它來(lái)開(kāi)發(fā)和維護企業(yè)級的應用。
我們要確保同事在正確的時(shí)間關(guān)閉了數據庫連接,還有他們不會(huì )不停的占有事務(wù),能在合適的分層上正確的處理好異常,能正確的獲得和釋放鎖,等等。
這些問(wèn)題任何一個(gè)單獨來(lái)看都不是什么大事。不過(guò)如果和領(lǐng)域內的復雜性一結合的話(huà),問(wèn)題就變得很棘手了,開(kāi)發(fā)資源緊張,難以維護。
如果把這些策略封裝成許多小塊的代碼,讓它們各自進(jìn)行約束管理的話(huà),會(huì )怎么樣呢?那我們就不用再不停的花費精力去實(shí)施策略了。這是個(gè)巨大的改進(jìn), 我們來(lái)看下函數式編程是如何做到的。
瘋狂的迭代
我們一直都在寫(xiě)各種迭代來(lái)處理列表,集合,還有map。在Java里使用迭代器再常見(jiàn)不過(guò)了,不過(guò)這太復雜了。它們不僅占用了好幾行代碼,還很難進(jìn)行封裝。
我們是如何遍歷集合并打印它們的?可以使用一個(gè)for循環(huán)。我們怎么從集合里過(guò)濾出一些元素?還是用for循環(huán),不過(guò)還需要額外增加一些可修改的變量。選出了這些值后,怎么用它們求出最終值,比如最小值,最大值,平均值之類(lèi)的?那還得再循環(huán),再修改變量。
這樣的迭代就是個(gè)萬(wàn)金油,啥都會(huì )點(diǎn),但樣樣稀松,F在Java為許多操作都專(zhuān)門(mén)提供了內建的迭代器:比如只做循環(huán)的,還有做map操作的,過(guò)濾值的,做reduce操作的,還有許多方便的函數比如 最大最小值,平均值等等。除此之外,這些操作還可以很好的組合起來(lái),因此我們可以將它們拼裝到一起來(lái)實(shí)現業(yè)務(wù)邏輯,這樣做既簡(jiǎn)單代碼量也少。而且寫(xiě)出來(lái)的代碼可讀性強,因為它從邏輯上和描述問(wèn)題的順序是一致的。我們在第二章,集合的使用,第19頁(yè)會(huì )看到幾個(gè)這樣的例子,這本書(shū)里這樣的例子也比比皆是。
應用策略
策略貫穿于整個(gè)企業(yè)級應用中。比如,我們需要確認某個(gè)操作已經(jīng)正確的進(jìn)行了安全認證,我們要保證事務(wù)能夠快速執行,并且正確的更新修改日志。這些任務(wù)通常最后就變成服務(wù)端的一段普通的代碼,就跟下面這個(gè)偽代碼差不多:
復制代碼 代碼如下:
Transaction transaction = getFromTransactionFactory();
//... operation to run within the transaction ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
這種處理方法有兩個(gè)問(wèn)題。首先,它通常導致了重復的工作量并且還增加了維護的成本。第二,很容易忘了業(yè)務(wù)代碼中可能會(huì )被拋出來(lái)的異常,可能會(huì )影響到事務(wù)的生命周期和修改日志的更新。這里應該使用try, finally塊來(lái)實(shí)現,不過(guò)每當有人動(dòng)了這塊代碼,我們又得重新確認這個(gè)策略沒(méi)有被破壞。
還有一種方法,我們可以去掉工廠(chǎng),把這段代碼放在它前面。不用再獲取事務(wù)對象,而是把執行的代碼傳給一個(gè)維護良好的函數,就像這樣:
復制代碼 代碼如下:
runWithinTransaction((Transaction transaction) -> {
//... operation to run within the transaction ...
});
這是你的一小步,但是省了一大堆事。檢查狀態(tài)同時(shí)更新日志的這個(gè)策略被抽象出來(lái)封裝到了runWithinTransaction方法里。我們給這個(gè)方法發(fā)送一段需要在事務(wù)上下文里運行的代碼。我們不用再擔心誰(shuí)忘了執行這個(gè)步驟或者沒(méi)有處理好異常。這個(gè)實(shí)施策略的函數已經(jīng)把這事搞定了。
我們將會(huì )在第五章中介紹如果使用lambda表達式來(lái)應用這樣的策略。
擴展策略
策略看起來(lái)無(wú)處不在。除了要應用它們外,企業(yè)級應用還需要對它們進(jìn)行擴展。我們希望能通過(guò)一些配置信息來(lái)增加或者刪除一些操作,換言之,就是能在模塊的核心邏輯執行前進(jìn)行處理。這在Java里很常見(jiàn),不過(guò)需要預先考慮到并設計好。
需要擴展的組件通常有一個(gè)或者多個(gè)接口。我們需要仔細設計接口以及實(shí)現類(lèi)的分層結構。這樣做可能效果很好,但是會(huì )留下一大堆需要維護的接口和類(lèi)。這樣的設計很容易變得笨重且難以維護,最終破壞擴展的初衷。
還有一種解決方法——函數式接口,以及l(fā)ambda表達式,我們可以用它們來(lái)設計可擴展的策略。我們不用非得創(chuàng )建新的接口或者都遵循同一個(gè)方法名,可以更聚焦要實(shí)現的業(yè)務(wù)邏輯,我們會(huì )在73頁(yè)的使用lambda表達式進(jìn)行裝飾中提到。
輕松實(shí)現并發(fā)
一個(gè)大型應用快到了發(fā)布里程碑的時(shí)候,突然一個(gè)嚴重的性能問(wèn)題浮出水面。團隊迅速定位出性能瓶頸點(diǎn)是出在一個(gè)處理海量數據的龐大的模塊里。團隊中有人建議說(shuō)如果能充分發(fā)掘多核的優(yōu)勢的話(huà)可以提高系統性能。不過(guò)如果這個(gè)龐大的模塊是用老的Java風(fēng)格寫(xiě)的話(huà),剛才這個(gè)建議帶來(lái)的喜悅很快就破滅了。
團隊很快意識到要這把這個(gè)龐然大物從串行執行改成并行需要費很大的精力,增加了額外的復雜度,還容易引起多線(xiàn)程相關(guān)的BUG。難道沒(méi)有一種提高性能的更好方式嗎?
有沒(méi)有可能串行和并行的代碼都是一樣的,不管選擇串行還是并行執行,就像按一下開(kāi)關(guān),表明一下想法就可以了?
聽(tīng)起來(lái)好像只有納尼亞里面能這樣,不過(guò)如果我們完全用函數式進(jìn)行開(kāi)發(fā)的話(huà),這一切都將成為現實(shí)。內置的迭代器和函數式風(fēng)格將掃清通往并行化的最后一道障礙。JDK的設計使得串行和并行執行的切換只需要一點(diǎn)不起眼的代碼改動(dòng)就可以實(shí)現,我們將會(huì )在145頁(yè)《完成并行化的飛躍》中提到。
講故事
在業(yè)務(wù)需求變成代碼實(shí)現的過(guò)程中會(huì )丟失大量的東西。丟失的越多,出錯的可能性和管理的成本就越高。如果代碼看起來(lái)就跟描述需求一樣,將會(huì )很方便閱讀,和需求人員討論也變的更簡(jiǎn)單,也更容易滿(mǎn)足他們的需求。
比如你聽(tīng)到產(chǎn)品經(jīng)理在說(shuō),”拿到所有股票的價(jià)格,找出價(jià)格大于500塊的,計算出能分紅的資產(chǎn)總和”。使用Java提供的新設施,可以這么寫(xiě):
復制代碼 代碼如下:
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
這個(gè)轉化過(guò)程幾乎是無(wú)損的,因為基本上也沒(méi)什么要轉化的。這是函數式在發(fā)揮作用,在本書(shū)中還會(huì )看到更多這樣的例子,尤其是第8章,使用lambda表達式來(lái)構建程序,137頁(yè)。
關(guān)注隔離
在系統開(kāi)發(fā)中,核心業(yè)務(wù)和它所需要的細粒度邏輯通常需要進(jìn)行隔離。比如說(shuō),一個(gè)訂單處理系統想要對不同的交易來(lái)源使用不同的計稅策略。把計稅和其余的處理邏輯進(jìn)行隔離會(huì )使得代碼重用性和擴展性更高。
在面向對象編程中我們把這個(gè)稱(chēng)之為關(guān)注隔離,通常用策略模式來(lái)解決這個(gè)問(wèn)題。解決方法一般就是創(chuàng )建一些接口和實(shí)現類(lèi)。
我們可以用更少的代碼來(lái)完成同樣的效果。我們還可以快速?lài)L試自己的產(chǎn)品思路,而不用上來(lái)就得搞出一堆代碼,停滯不前。我們將在63頁(yè)的,使用lambda表達式進(jìn)行關(guān)注隔離中進(jìn)一步探討如果通過(guò)輕量級函數來(lái)創(chuàng )建這種模式以及進(jìn)行關(guān)注隔離。
惰性求值
開(kāi)發(fā)企業(yè)級應用時(shí),我們可能會(huì )與WEB服務(wù)進(jìn)行交互,調用數據庫,處理XML等等。我們要執行的操作有很多,不過(guò)并不是所有時(shí)候都全部需要。避免某些操作或者至少延遲一些暫時(shí)不需要的操作是提高性能或者減少程序啟動(dòng),響應時(shí)間的一個(gè)最簡(jiǎn)單的方式。
這只是個(gè)小事,但用純OOP的方式來(lái)實(shí)現還需要費一番工夫。為了延遲一些重量級對象的初始化,我們要處理各種對象引用 ,檢查空指針等等。
不過(guò),如果使用了新的Optinal類(lèi)和它提供的一些函數式風(fēng)格的API,這個(gè)過(guò)程將變得很簡(jiǎn)單,代碼也更清晰明了,我們會(huì )在105頁(yè)的延遲初始化中討論這個(gè)。
提高可測性
代碼的處理邏輯越少,容易被改錯的地方當然也越少。一般來(lái)說(shuō)函數式的代碼比較容易修改,測試起來(lái)也較簡(jiǎn)單。
另外,就像第4章,使用lambda表達式進(jìn)行設計和第5章資源的使用中那樣,lambda表達式可以作為一種輕量級的mock對象,讓異常測試變得更清晰易懂。lambda表達式還可以作為一個(gè)很好的測試輔助工具。很多常見(jiàn)的測試用例都可以接受并處理lambda表達式。這樣寫(xiě)的測試用例能夠抓住需要回歸測試的功能的本質(zhì)。同時(shí),需要測試的各種實(shí)現都可以通過(guò)傳入不同的lambda表達式來(lái)完成。
JDK自己的自動(dòng)化測試用例也是lambda表達式的一個(gè)很好的應用范例——想了解更多的話(huà)可以看下OpenJDK倉庫里的源代碼。通過(guò)這些測試程序可以看到lambda表達式是如何將測試用例的關(guān)鍵行為進(jìn)行參數化;比如,它們是這樣構建測試程序的,“新建一個(gè)結果的容器”,然后“對一些參數化的后置條件進(jìn)行檢查”。
我們已經(jīng)看到,函數式編程不僅能讓我們寫(xiě)出高質(zhì)量的代碼,還能優(yōu)雅的解決開(kāi)發(fā)過(guò)程中的各種難題。這就是說(shuō),開(kāi)發(fā)程序將變得更快更簡(jiǎn)單,出錯也更少——只要你能遵守我們后面將要介紹到的幾條準則。
第四節:進(jìn)化而非革命
我們用不著(zhù)轉向別的語(yǔ)言,就能享受函數式編程帶來(lái)的好處;需要改變的只是使用Java的一些方式。C++,Java,C#這些語(yǔ)言都支持命令式和面向對象的編程。不過(guò)現在它們都開(kāi)始投入函數式編程的懷抱里了。我們剛才已經(jīng)看到了這兩種風(fēng)格的代碼,并討論了函數式編程能帶來(lái)的好處,F在我們來(lái)看下它的一些關(guān)鍵概念和例子來(lái)幫助我們學(xué)習這種新的風(fēng)格。
Java語(yǔ)言的開(kāi)發(fā)團隊花費了大量的時(shí)間和精力把函數式編程的能力添加到了Java語(yǔ)言和JDK里。要享受它帶來(lái)的好處,我們得先介紹幾個(gè)新的概念。我們只要遵循下面幾條規則就能提升我們的代碼質(zhì)量:
1.聲明式
2.提倡不可變性
3.避免副作用
4.優(yōu)先使用表達式而不是語(yǔ)句
5.使用高階函數進(jìn)行設計
我們來(lái)看下這幾條實(shí)踐準則。
聲明式
我們所熟悉的命令式編程的核心就是可變性和命令驅動(dòng)的編程。我們創(chuàng )建變量,然后不斷修改它們的值。我們還提供了要執行的詳細的指令,比如生成迭代的索引標志,增加它的值,檢查循環(huán)是否結束,更新數組的第N個(gè)元素等。在過(guò)去由于工具的特性和硬件的限制,我們只能這么寫(xiě)代碼。 我們也看到了,在一個(gè)不可變集合上,聲明式的contains方法比命令式的更容易使用。所有的難題和低級的操作都在庫函數里來(lái)實(shí)現了,我們不用再關(guān)心這些細節。就沖著(zhù)簡(jiǎn)單這點(diǎn),我們也應該使用聲明式編程。不可變性和聲明式編程是函數式編程的精髓,現在Java終于把它變成了現實(shí)。
提倡不可變性
變量可變的代碼會(huì )有很多活動(dòng)路徑。改的東西越多,越容易破壞原有的結構,并引入更多的錯誤。有多個(gè)變量被修改的代碼難于理解也很難進(jìn)行并行化。不可變性從根本上消除了這些煩惱。 Java支持不可變性但沒(méi)有強制要求——但我們可以。我們需要改變修改對象狀態(tài)這個(gè)舊習慣。我們要盡可能的使用不可變的對象。 聲明變量,成員和參數的時(shí)候,盡量聲明為final的,就像Joshua Bloch在” Effective Java“里說(shuō)的那句名言那樣,“把對象當成不可變的吧”。 當創(chuàng )建對象的時(shí)候,盡量創(chuàng )建不可變的對象,比如String這樣的。創(chuàng )建集合的時(shí)候,也盡量創(chuàng )建不可變或者無(wú)法修改的集合,比如用Arrays.asList()和Collections的unmodifiableList()這樣的方法。 避免了可變性我們才可以寫(xiě)出純粹的函數——也就是,沒(méi)有副作用的函數。
避免副作用
假設你在寫(xiě)一段代碼到網(wǎng)上去抓取一支股票的價(jià)格然后寫(xiě)到一個(gè)共享變量里。如果我們有很多價(jià)格要抓取,我們得串行的執行這些費時(shí)的操作。如果我們想借助多線(xiàn)程的能力,我們得處理線(xiàn)程和同步帶來(lái)的麻煩事,防止出現競爭條件。最后的結果是程序的性能很差,為了維護線(xiàn)程而廢寢忘食。如果消除了副作用,我們完全可以避免這些問(wèn)題。 沒(méi)有副作用的函數推崇的是不可變性,在它的作用域內不會(huì )修改任何輸入或者別的東西。這種函數可讀性強,錯誤少,容易優(yōu)化。由于沒(méi)有副作用,也不用再擔心什么競爭條件或者并發(fā)修改了。不僅如此,我們還可以很容易并行執行這些函數,我們將在145頁(yè)的來(lái)討論這個(gè)。
優(yōu)先使用表達式
語(yǔ)句是個(gè)燙手的山芋,因為它強制進(jìn)行修改。表達式提升了不可變性和函數組合的能力。比如,我們先用for語(yǔ)句計算折扣后的總價(jià)。這樣的代碼導致了可變性以及冗長(cháng)的代碼。使用map和sum方法的表達性更強的聲明式的版本后,不僅避免了修改操作,同時(shí)還能把函數串聯(lián)起來(lái)。 寫(xiě)代碼的時(shí)候應該盡量使用表達式,而不是語(yǔ)句。這樣使得代碼更簡(jiǎn)潔易懂。代碼會(huì )順著(zhù)業(yè)務(wù)邏輯執行,就像我們描述問(wèn)題的時(shí)候那樣。如果需求變動(dòng),簡(jiǎn)潔的版本無(wú)疑更容易修改。
使用高階函數進(jìn)行設計
Java不像Haskell那些函數式語(yǔ)言那樣強制要求不可變,它允許我們修改變量。因此,Java不是,也永遠不會(huì )是,一個(gè)純粹的函數式編程語(yǔ)言。然而,我們可以在Java里使用高階函數進(jìn)行函數式編程。 高階函數使得重用更上一層樓。有了高階函數我們可以很方便的重用那些小而專(zhuān),內聚性強的成熟的代碼。 在OOP中我們習慣了給方法傳遞給對象,在方法里面創(chuàng )建新的對象,然后返回對象。高階函數對函數做的事情就跟方法對對象做的一樣。有了高階函數我們可以。
1.把函數傳給函數
2.在函數內創(chuàng )建新的函數
3.在函數內返回函數
我們已經(jīng)見(jiàn)過(guò)一個(gè)把函數傳參給另一個(gè)函數的例子了,在后面我們還會(huì )看到創(chuàng )建函數和返回函數的示例。我們先再看一遍“把函數傳參給函數”的那個(gè)例子:
復制代碼 代碼如下:
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
report erratum discuss
.reduce(BigDecimal.ZERO, BigDecimal::add);
在這段代碼中我們把函數price -> price.multiply(BigDecimal.valueOf(0.9)),傳給了map函數。傳遞的這個(gè)函數是在調用高階函數map的時(shí)候才創(chuàng )建的。通常來(lái)說(shuō)一個(gè)函數有函數體,函數名,參數列表,返回值。這個(gè)實(shí)時(shí)創(chuàng )建的函數有一個(gè)參數列表后面跟著(zhù)一個(gè)箭頭(->),然后就是很短的一段函數體了。參數的類(lèi)型由Java編譯器來(lái)進(jìn)行推導,返回的類(lèi)型也是隱式的。這是個(gè)匿名函數,它沒(méi)有名字。不過(guò)我們不叫它匿名函數,我們稱(chēng)之為lambda表達式。 匿名函數作為傳參在Java并不算是什么新鮮事;我們之前也經(jīng)常傳遞匿名內部類(lèi)。即使匿名類(lèi)只有一個(gè)方法,我們還是得走一遍創(chuàng )建類(lèi)的儀式,然后對它進(jìn)行實(shí)例化。有了lambda表達式我們可以享受輕量級的語(yǔ)法了。不僅如此,我們之前總是習慣把一些概念抽象成各種對象,現在我們可以將一些行為抽象成lambda表達式了。 用這種編碼風(fēng)格進(jìn)行程序設計還是需要費些腦筋的。我們得把已經(jīng)根深蒂固的命令式思維轉變成函數式的。開(kāi)始的時(shí)候可能有點(diǎn)痛苦,不過(guò)很快你就會(huì )習慣它了,隨著(zhù)不斷的深入,那些非函數式的API逐漸就被拋到腦后了。 這個(gè)話(huà)題就先到這吧,我們來(lái)看看Java是如何處理lambda表達式的。我們之前總是把對象傳給方法,現在我們可以把函數存儲起來(lái)并傳遞它們。 我們來(lái)看下Java能夠將函數作為參數背后的秘密。
第五節:加了點(diǎn)語(yǔ)法糖
用Java原有的功能也是可以實(shí)現這些的,不過(guò)lambda表達式加了點(diǎn)語(yǔ)法糖,省掉了一些步驟,使我們的工作更簡(jiǎn)單了。這樣寫(xiě)出的代碼不僅開(kāi)發(fā)更快,也更能表達我們的想法。 過(guò)去我們用的很多接口都只有一個(gè)方法:像Runnable, Callable等等。這些接口在JDK庫中隨處可見(jiàn),使用它們的地方通常用一個(gè)函數就能搞定。原來(lái)的這些只需要一個(gè)單方法接口的庫函數現在可以傳遞輕量級函數了,多虧了這個(gè)通過(guò)函數式接口提供的語(yǔ)法糖。 函數式接口是只有一個(gè)抽象方法的接口。再看下那些只有一個(gè)方法的接口,Runnable,Callable等,都適用這個(gè)定義。JDK8里面有更多這類(lèi)的接口——Function, Predicate, Comsumer, Supplier等(157頁(yè),附錄1有更詳細的接口列表)。函數式接口可以有多個(gè)static方法,和default方法,這些方法是在接口里面實(shí)現的。 我們可以用@FunctionalInterface注解來(lái)標注一個(gè)函數式接口。編譯器不使用這個(gè)注解,不過(guò)有了它可以更明確的標識這個(gè)接口的類(lèi)型。不止如此,如果我們用這個(gè)注解標注了一個(gè)接口,編譯器會(huì )強制校驗它是否符合函數式接口的規則。 如果一個(gè)方法接收函數式接口作為參數,我們可以傳遞的參數包括:
1.匿名內部類(lèi),最古老的方式
2.lambda表達式,就像我們在map方法里那樣
3.方法或者構造器的引用(后面我們會(huì )講到)
如果方法的參數是函數式接口的話(huà),編譯器會(huì )很樂(lè )意接受lambda表達式或者方法引用作為參數。 如果我們把一個(gè)lambda表達式傳遞給一個(gè)方法,編譯器會(huì )先把這個(gè)表達式轉化成對應的函數式接口的一個(gè)實(shí)例。這個(gè)轉化可不止是生成一個(gè)內部類(lèi)而已。同步生成的這個(gè)實(shí)例的方法對應于參數的函數式接口的抽象方法。比如,map方法接收函數式接口Function作為參數。在調用map方法時(shí),java編譯器會(huì )同步生成它,就像下圖所示的一樣。
lambda表達式的參數必須和接口的抽象方法的參數匹配。這個(gè)生成的方法將返回lambda表達式的結果。如果返回類(lèi)型不直接匹配抽象方法的話(huà),這個(gè)方法會(huì )把返回值轉化成合適的類(lèi)型。 我們已經(jīng)大概了解了下lambda表達式是如何傳遞給方法的。我們先來(lái)快速回顧一下剛講的內容,然后開(kāi)始我們lambda表達式的探索之旅。
總結
這是Java一個(gè)全新的領(lǐng)域。通過(guò)高階函數,我們現在可以寫(xiě)出優(yōu)雅流利的函數式風(fēng)格的代碼了。這樣寫(xiě)出的代碼,簡(jiǎn)潔易懂,錯誤少,利于維護和并行化。Java編譯器發(fā)揮了它的魔力,在接收函數式接口參數的地方,我們可以傳入lambda表達式或者方法引用。 我們現在可以進(jìn)入lambda表達式以及為之改造的JDK庫的世界來(lái)感覺(jué)它們的樂(lè )趣了。在下一章中,我們將從編程里面最常見(jiàn)的集合操作開(kāi)始,發(fā)揮lambda表達式的威力。
【java中lambda表達式】相關(guān)文章:
詳解Java中的Lambda表達式10-20
Java 正則表達式09-23
java正則表達式10-24
常用java正則表達式介紹08-07
Java的運算符和表達式10-08
幫助你駕馭Java正則表達式08-01
Java賦值運算符與賦值表達式09-27
Java如何正則表達式分析讀取網(wǎng)頁(yè)內容06-29
Java 正則表達式的總結和一些小例子10-16