當前位置:才華齋>計算機>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表示式的威力。