當前位置:才華齋>計算機>java語言>

java中lambda表達式

java語言 閲讀(3.14W)

第一章 你好,lambda表達式!

java中lambda表達式

第一節

Java的編碼風格正面臨着翻天覆地的變化。

我們每天的工作將會變成更簡單方便,更富表現力。Java這種新的編程方式早在數十年前就已經出現在別的編程語言裏面了。這些新特性引入Java後,我們可以寫出更簡潔,優雅,表達性更強,錯誤更少的代碼。我們可以用更少的代碼來實現各種策略和設計模式。

在本書中我們將通過日常編程中的一些例子來探索函數式風格的編程。在使用這種全新的優雅的方式進行設計編碼之前,我們先來看下它到底好在哪裏。

改變了你的思考方式

命令式風格——Java語言從誕生之初就一直提供的是這種方式。使用這種風格的話,我們得告訴Java每一步要做什麼,然後看着它切實的一步步執行下去。這樣做當然很好,就是顯得有點初級。代碼看起來有點囉嗦,我們希望這個語言能變得稍微智能一點;我們應該直接告訴它我們想要什麼,而不是告訴它如何去做。好在現在Java終於可以幫我們實現這個願望了。我們先來看幾個例子,瞭解下這種風格的優點和不同之處。

正常的方式

我們先從兩個熟悉的例子來開始。這是用命令的方式來查看芝加哥是不是指定的城市集合裏——記住,本書中列出的代碼只是部分片段而已。

複製代碼 代碼如下:

boolean found = false;

for(String city : cities) {

if(ls("Chicago")) {

found = true;

break;

}

}

tln("Found chicago" + found);

這個命令式的版本看起來有點囉嗦而且初級;它分成好幾個執行部分。先是初始化一個叫found的布爾標記,然後遍歷集合裏的每一個元素;如果發現我們要找的城市了,設置下這個標記,然後跳出循環體;最後打印出查找的結果。

一種更好的方式

細心的Java程序員看完這段代碼後,很快會想到一種更簡潔明瞭的方式,就像這樣:

複製代碼 代碼如下:

tln("Found chicago" + ains("Chicago"));

這也是一種命令式風格的寫法——contains方法直接就幫我們搞定了。

實際改進的地方

代碼這麼寫有這幾個好處:

1.不用再搗鼓那個可變的變量了

2.將迭代封裝到了底層

3.代碼更簡潔

4.代碼更清晰,更聚焦

5.少走彎路,代碼和業務需求結合更密切

6.不易出錯

7.易於理解和維護

來個複雜點的例子

這個例子太簡單了,命令式查詢一個元素是否存在於某個集合在Java裏隨處可見。現在假設我們要用命令式編程來進行些更高級的操作,比如解析文件 ,和數據庫交互,調用WEB服務,併發編程等等。現在我們用Java可以寫出更簡潔優雅同時出錯更少的代碼,更不只是這種簡單的場景。

老的方式

我們來看下另一個例子。我們定義了一系列價格,並通過不同的方式來計算打折後的總價。

複製代碼 代碼如下:

final Listprices = st(

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 = ;

for(BigDecimal price : prices) {

if(areTo(eOf(20)) > 0)

totalOfDiscountedPrices =

(iply(eOf(0.9)));

}

tln("Total of discounted prices: " + totalOfDiscountedPrices);

這個代碼很熟悉吧;先用一個變量來存儲總價;然後遍歷所有的價格,找出大於20塊的,算出它們的折扣價,並加到總價裏面;最後打印出折扣後的總價。

下面是程序的輸出:

複製代碼 代碼如下:

Total of discounted prices: 67.5

結果完全正確,不過這樣的代碼有點亂。這並不是我們的錯,我們只能用已有的方式來寫。不過這樣的代碼實在有點初級,它不僅存在基本類型偏執,而且還違反了單一職責原則。如果你是在家工作並且家裏還有想當碼農的小孩的話,你可得把你的代碼藏好了,萬一他們看見了會很失望地歎氣道,“你是靠這些玩意兒餬口的?”

還有更好的方式

我們還能做的更好——並且要好很多。我們的代碼有點像需求規範。這樣能縮小業務需求和實現的代碼之間的差距,減少了需求被誤讀的可能性。

我們不再讓Java去創建一個變量然後沒完沒了的給它賦值了,我們要從一個更高層次的抽象去與它溝通,就像下面的這段代碼。

複製代碼 代碼如下:

final BigDecimal totalOfDiscountedPrices =

am()

er(price -> areTo(eOf(20)) > 0)

(price -> iply(eOf(0.9)))

ce(, BigDecimal::add);

tln("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提供的新設施,可以這麼寫:

複製代碼 代碼如下:

(StockUtil::getprice)er(StockUtil::priceIsLessThan500)()

這個轉化過程幾乎是無損的,因為基本上也沒什麼要轉化的。這是函數式在發揮作用,在本書中還會看到更多這樣的例子,尤其是第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這樣的。創建集合的時候,也儘量創建不可變或者無法修改的集合,比如用st()和Collections的unmodifiableList()這樣的方法。 避免了可變性我們才可以寫出純粹的函數——也就是,沒有副作用的函數。

避免副作用

假設你在寫一段代碼到網上去抓取一支股票的價格然後寫到一個共享變量裏。如果我們有很多價格要抓取,我們得串行的執行這些費時的操作。如果我們想借助多線程的能力,我們得處理線程和同步帶來的麻煩事,防止出現競爭條件。最後的結果是程序的性能很差,為了維護線程而廢寢忘食。如果消除了副作用,我們完全可以避免這些問題。 沒有副作用的函數推崇的是不可變性,在它的作用域內不會修改任何輸入或者別的東西。這種函數可讀性強,錯誤少,容易優化。由於沒有副作用,也不用再擔心什麼競爭條件或者併發修改了。不僅如此,我們還可以很容易並行執行這些函數,我們將在145頁的來討論這個。

優先使用表達式

語句是個燙手的山芋,因為它強制進行修改。表達式提升了不可變性和函數組合的能力。比如,我們先用for語句計算折扣後的總價。這樣的代碼導致了可變性以及宂長的代碼。使用map和sum方法的表達性更強的聲明式的版本後,不僅避免了修改操作,同時還能把函數串聯起來。 寫代碼的時候應該儘量使用表達式,而不是語句。這樣使得代碼更簡潔易懂。代碼會順着業務邏輯執行,就像我們描述問題的時候那樣。如果需求變動,簡潔的版本無疑更容易修改。

使用高階函數進行設計

Java不像Haskell那些函數式語言那樣強制要求不可變,它允許我們修改變量。因此,Java不是,也永遠不會是,一個純粹的函數式編程語言。然而,我們可以在Java裏使用高階函數進行函數式編程。 高階函數使得重用更上一層樓。有了高階函數我們可以很方便的重用那些小而專,內聚性強的成熟的代碼。 在OOP中我們習慣了給方法傳遞給對象,在方法裏面創建新的對象,然後返回對象。高階函數對函數做的事情就跟方法對對象做的一樣。有了高階函數我們可以。

1.把函數傳給函數

2.在函數內創建新的函數

3.在函數內返回函數

我們已經見過一個把函數傳參給另一個函數的例子了,在後面我們還會看到創建函數和返回函數的示例。我們先再看一遍“把函數傳參給函數”的那個例子:

複製代碼 代碼如下:

am()

er(price -> areTo(eOf(20)) > 0) (price -> iply(eOf(0.9)))

report erratum discuss

ce(, BigDecimal::add);

在這段代碼中我們把函數price -> iply(eOf(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.匿名內部類,最古老的方式

da表達式,就像我們在map方法裏那樣

3.方法或者構造器的引用(後面我們會講到)

如果方法的參數是函數式接口的話,編譯器會很樂意接受lambda表達式或者方法引用作為參數。 如果我們把一個lambda表達式傳遞給一個方法,編譯器會先把這個表達式轉化成對應的函數式接口的一個實例。這個轉化可不止是生成一個內部類而已。同步生成的這個實例的方法對應於參數的函數式接口的抽象方法。比如,map方法接收函數式接口Function作為參數。在調用map方法時,java編譯器會同步生成它,就像下圖所示的一樣。

lambda表達式的參數必須和接口的抽象方法的參數匹配。這個生成的方法將返回lambda表達式的結果。如果返回類型不直接匹配抽象方法的話,這個方法會把返回值轉化成合適的類型。 我們已經大概瞭解了下lambda表達式是如何傳遞給方法的。我們先來快速回顧一下剛講的內容,然後開始我們lambda表達式的探索之旅。

總結

這是Java一個全新的領域。通過高階函數,我們現在可以寫出優雅流利的函數式風格的代碼了。這樣寫出的代碼,簡潔易懂,錯誤少,利於維護和並行化。Java編譯器發揮了它的魔力,在接收函數式接口參數的地方,我們可以傳入lambda表達式或者方法引用。 我們現在可以進入lambda表達式以及為之改造的JDK庫的世界來感覺它們的樂趣了。在下一章中,我們將從編程裏面最常見的集合操作開始,發揮lambda表達式的威力。