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