亚洲精品中文字幕无乱码_久久亚洲精品无码AV大片_最新国产免费Av网址_国产精品3级片

java語言

java中l(wèi)ambda表達式

時間:2024-10-22 21:28:43 java語言 我要投稿
  • 相關(guān)推薦

java中l(wèi)ambda表達式

  第一章 你好,lambda表達式!

  第一節(jié)

  Java的編碼風(fēng)格正面臨著翻天覆地的變化。

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

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

  改變了你的思考方式

  命令式風(fēng)格——Java語言從誕生之初就一直提供的是這種方式。使用這種風(fēng)格的話,我們得告訴Java每一步要做什么,然后看著它切實的一步步執(zhí)行下去。這樣做當然很好,就是顯得有點初級。代碼看起來有點啰嗦,我們希望這個語言能變得稍微智能一點;我們應(yīng)該直接告訴它我們想要什么,而不是告訴它如何去做。好在現(xiàn)在Java終于可以幫我們實現(xiàn)這個愿望了。我們先來看幾個例子,了解下這種風(fēng)格的優(yōu)點和不同之處。

  正常的方式

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

  復(fù)制代碼 代碼如下:

  boolean found = false;

  for(String city : cities) {

  if(city.equals("Chicago")) {

  found = true;

  break;

  }

  }

  System.out.println("Found chicago" + found);

  這個命令式的版本看起來有點啰嗦而且初級;它分成好幾個執(zhí)行部分。先是初始化一個叫found的布爾標記,然后遍歷集合里的每一個元素;如果發(fā)現(xiàn)我們要找的城市了,設(shè)置下這個標記,然后跳出循環(huán)體;最后打印出查找的結(jié)果。

  一種更好的方式

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

  復(fù)制代碼 代碼如下:

  System.out.println("Found chicago" + cities.contains("Chicago"));

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

  實際改進的地方

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

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

  2.將迭代封裝到了底層

  3.代碼更簡潔

  4.代碼更清晰,更聚焦

  5.少走彎路,代碼和業(yè)務(wù)需求結(jié)合更密切

  6.不易出錯

  7.易于理解和維護

  來個復(fù)雜點的例子

  這個例子太簡單了,命令式查詢一個元素是否存在于某個集合在Java里隨處可見,F(xiàn)在假設(shè)我們要用命令式編程來進行些更高級的操作,比如解析文件 ,和數(shù)據(jù)庫交互,調(diào)用WEB服務(wù),并發(fā)編程等等,F(xiàn)在我們用Java可以寫出更簡潔優(yōu)雅同時出錯更少的代碼,更不只是這種簡單的場景。

  老的方式

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

  復(fù)制代碼 代碼如下:

  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"));

  假設(shè)超過20塊的話要打九折,我們先用普通的方式實現(xiàn)一遍。

  復(fù)制代碼 代碼如下:

  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塊的,算出它們的折扣價,并加到總價里面;最后打印出折扣后的總價。

  下面是程序的輸出:

  復(fù)制代碼 代碼如下:

  Total of discounted prices: 67.5

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

  還有更好的方式

  我們還能做的更好——并且要好很多。我們的代碼有點像需求規(guī)范。這樣能縮小業(yè)務(wù)需求和實現(xiàn)的代碼之間的差距,減少了需求被誤讀的可能性。

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

  復(fù)制代碼 代碼如下:

  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塊的價格,把它們轉(zhuǎn)化成折扣價,然后加起來。這段代碼和我們描述需求的流程簡直一模一樣。Java里還可以很方便的把一行長的代碼折疊起來,根據(jù)方法名前面的點號進行按行對齊,就像上面那樣。

  代碼非常簡潔,不過我們用到了Java8里面的很多新東西。首先,我們調(diào)用 了價格列表的一個stream方法。這打開了一扇大門,門后邊有數(shù)不盡的便捷的迭代器,這個我們在后面會繼續(xù)討論。

  我們用了一些特殊的方法,比如filter和map,而不是直接的遍歷整個列表。這些方法不像我們以前用的JDK里面的那些,它們接受一個匿名的函數(shù)——lambda表達式——作為參數(shù)。(后面我們會深入的展開討論)。我們調(diào)用reduce()方法來計算map()方法返回的價格的總和。

  就像contains方法那樣,循環(huán)體被隱藏起來了。不過map方法(以及filter方法)則更復(fù)雜得多 。它對價格列表中的每一個價格,調(diào)用了傳進來的lambda表達式進行計算,把結(jié)果放到一個新的集合里面。最后我們在這個新的集合上調(diào)用 reduce方法得出最終的結(jié)果。

  這是以上代碼的輸出結(jié)果:

  復(fù)制代碼 代碼如下:

  Total of discounted prices: 67.5

  改進的地方

  這和前面的實現(xiàn)相比改進明顯:

  1.結(jié)構(gòu)良好而不混亂

  2.沒有低級操作

  3.易于增強或者修改邏輯

  4.由方法庫來進行迭代

  5.高效;循環(huán)體惰性求值

  6.易于并行化

  下面我們會說到Java是如何實現(xiàn)這些的。

  lambda表達式來拯救世界了

  lambda表達式是讓我們遠離命令式編程煩惱的快捷鍵。Java提供的這個新特性,改變了我們原有的編程方式,使得我們寫出的代碼不僅簡潔優(yōu)雅,不易出錯,而且效率更高,易于優(yōu)化改進和并行化。

  第二節(jié):函數(shù)式編程的最大收獲

  函數(shù)式風(fēng)格的代碼有更高的信噪比;寫的代碼更少了,但每一行或者每個表達式做的卻更多了。比命令式編程相比,函數(shù)式編程讓我們獲益良多:

  避免了對變量的顯式的修改或賦值,這些通常是BUG的根源,并導(dǎo)致代碼很難并行化。在命令行編程中我們在循環(huán)體內(nèi)不停的對totalOfDiscountedPrices變量賦值。在函數(shù)式風(fēng)格里,代碼不再出現(xiàn)顯式的修改操作。變量修改的越少,代碼的BUG就越少。

  函數(shù)式風(fēng)格的代碼可以輕松的實現(xiàn)并行化。如果計算很費時,我們可以很容易讓列表中的元素并發(fā)的執(zhí)行。如果我們想把命令式的代碼并行化,我們還得擔心并發(fā)修改totalOfDiscountedPrices變量帶來的問題。在函數(shù)式編程中我們只會在完全處理完后才訪問這個變量,這樣就消除了線程安全的隱患。

  代碼的表達性更強。命令式編程要分成好幾個步驟要說明要做什么——創(chuàng)建一個初始化的值,遍歷價格,把折扣價加到變量上等等——而函數(shù)式的話只需要讓列表的map方法返回一個包括折扣價的新的列表然后進行累加就可以了。

  函數(shù)式編程更簡潔;和命令式相比同樣的結(jié)果只需要更少的代碼就能完成。代碼更簡潔意味著寫的代碼少了,讀的也少了,維護的也少了——看下第7頁的"簡潔少就是簡潔了嗎"。

  函數(shù)式的代碼更直觀——讀代碼就像描述問題一樣——一旦我們熟悉語法后就很容易能看懂。map方法對集合的每個元素都執(zhí)行了一遍給定的函數(shù)(計算折扣價),然后返回結(jié)果集,就像下圖演示的這樣。

  圖1——map對集合中的每個元素執(zhí)行給定的函數(shù)

  有了lambda表達式之后,我們可以在Java里充分發(fā)揮函數(shù)式編程的威力。使用函數(shù)式風(fēng)格,就能寫出表達性更佳,更簡潔,賦值操作更少,錯誤更少的代碼了。

  支持面向?qū)ο缶幊淌荍ava一個主要的優(yōu)點。函數(shù)式編程和面向?qū)ο缶幊滩⒉慌懦。真正的風(fēng)格變化是從命令行編程轉(zhuǎn)到聲明式編程。在Java 8里,函數(shù)式和面向?qū)ο罂梢杂行У娜诤系揭黄。我們可以繼續(xù)用OOP的風(fēng)格來對領(lǐng)域?qū)嶓w以及它們的狀態(tài),關(guān)系進行建模。除此之外,我們還可以對行為或者狀態(tài)的轉(zhuǎn)變,工作流和數(shù)據(jù)處理用函數(shù)來進行建模,建立復(fù)合函數(shù)。

  第三節(jié):為什么要用函數(shù)式風(fēng)格?

  我們看到了函數(shù)式編程的各項優(yōu)點,不過使用這種新的風(fēng)格劃得來嗎?這只是個小改進還是說換頭換面?在真正在這上面花費工夫前,還有很多現(xiàn)實的問題需要解答。

  復(fù)制代碼 代碼如下:

  小明問到:

  代碼少就是簡潔了嗎?

  簡潔是少而不亂,歸根結(jié)底是說要能有效的表達意圖。它帶來的好處意義深遠。

  寫代碼就好像把配料堆到一起,簡潔就是說能把配料調(diào)成調(diào)料。要寫出簡潔的代碼可得下得狠工夫。讀的代碼是少了,真正有用的代碼對你是透明的。一段很難理解或者隱藏細節(jié)的短代碼只能說是簡短而不是簡潔。

  簡潔的代碼竟味著敏捷的設(shè)計。簡潔的代碼少了那些繁文縟節(jié)。這是說我們可以對想法進行快速嘗試,如果不錯就繼續(xù),如果效果不佳就迅速跳過。

  用Java寫代碼并不難,語法簡單。而且我們也已經(jīng)對現(xiàn)有的庫和API很了如指掌了。真正難的是要拿它來開發(fā)和維護企業(yè)級的應(yīng)用。

  我們要確保同事在正確的時間關(guān)閉了數(shù)據(jù)庫連接,還有他們不會不停的占有事務(wù),能在合適的分層上正確的處理好異常,能正確的獲得和釋放鎖,等等。

  這些問題任何一個單獨來看都不是什么大事。不過如果和領(lǐng)域內(nèi)的復(fù)雜性一結(jié)合的話,問題就變得很棘手了,開發(fā)資源緊張,難以維護。

  如果把這些策略封裝成許多小塊的代碼,讓它們各自進行約束管理的話,會怎么樣呢?那我們就不用再不停的花費精力去實施策略了。這是個巨大的改進, 我們來看下函數(shù)式編程是如何做到的。

  瘋狂的迭代

  我們一直都在寫各種迭代來處理列表,集合,還有map。在Java里使用迭代器再常見不過了,不過這太復(fù)雜了。它們不僅占用了好幾行代碼,還很難進行封裝。

  我們是如何遍歷集合并打印它們的?可以使用一個for循環(huán)。我們怎么從集合里過濾出一些元素?還是用for循環(huán),不過還需要額外增加一些可修改的變量。選出了這些值后,怎么用它們求出最終值,比如最小值,最大值,平均值之類的?那還得再循環(huán),再修改變量。

  這樣的迭代就是個萬金油,啥都會點,但樣樣稀松,F(xiàn)在Java為許多操作都專門提供了內(nèi)建的迭代器:比如只做循環(huán)的,還有做map操作的,過濾值的,做reduce操作的,還有許多方便的函數(shù)比如 最大最小值,平均值等等。除此之外,這些操作還可以很好的組合起來,因此我們可以將它們拼裝到一起來實現(xiàn)業(yè)務(wù)邏輯,這樣做既簡單代碼量也少。而且寫出來的代碼可讀性強,因為它從邏輯上和描述問題的順序是一致的。我們在第二章,集合的使用,第19頁會看到幾個這樣的例子,這本書里這樣的例子也比比皆是。

  應(yīng)用策略

  策略貫穿于整個企業(yè)級應(yīng)用中。比如,我們需要確認某個操作已經(jīng)正確的進行了安全認證,我們要保證事務(wù)能夠快速執(zhí)行,并且正確的更新修改日志。這些任務(wù)通常最后就變成服務(wù)端的一段普通的代碼,就跟下面這個偽代碼差不多:

  復(fù)制代碼 代碼如下:

  Transaction transaction = getFromTransactionFactory();

  //... operation to run within the transaction ...

  checkProgressAndCommitOrRollbackTransaction();

  UpdateAuditTrail();

  這種處理方法有兩個問題。首先,它通常導(dǎo)致了重復(fù)的工作量并且還增加了維護的成本。第二,很容易忘了業(yè)務(wù)代碼中可能會被拋出來的異常,可能會影響到事務(wù)的生命周期和修改日志的更新。這里應(yīng)該使用try, finally塊來實現(xiàn),不過每當有人動了這塊代碼,我們又得重新確認這個策略沒有被破壞。

  還有一種方法,我們可以去掉工廠,把這段代碼放在它前面。不用再獲取事務(wù)對象,而是把執(zhí)行的代碼傳給一個維護良好的函數(shù),就像這樣:

  復(fù)制代碼 代碼如下:

  runWithinTransaction((Transaction transaction) -> {

  //... operation to run within the transaction ...

  });

  這是你的一小步,但是省了一大堆事。檢查狀態(tài)同時更新日志的這個策略被抽象出來封裝到了runWithinTransaction方法里。我們給這個方法發(fā)送一段需要在事務(wù)上下文里運行的代碼。我們不用再擔心誰忘了執(zhí)行這個步驟或者沒有處理好異常。這個實施策略的函數(shù)已經(jīng)把這事搞定了。

  我們將會在第五章中介紹如果使用lambda表達式來應(yīng)用這樣的策略。

  擴展策略

  策略看起來無處不在。除了要應(yīng)用它們外,企業(yè)級應(yīng)用還需要對它們進行擴展。我們希望能通過一些配置信息來增加或者刪除一些操作,換言之,就是能在模塊的核心邏輯執(zhí)行前進行處理。這在Java里很常見,不過需要預(yù)先考慮到并設(shè)計好。

  需要擴展的組件通常有一個或者多個接口。我們需要仔細設(shè)計接口以及實現(xiàn)類的分層結(jié)構(gòu)。這樣做可能效果很好,但是會留下一大堆需要維護的接口和類。這樣的設(shè)計很容易變得笨重且難以維護,最終破壞擴展的初衷。

  還有一種解決方法——函數(shù)式接口,以及l(fā)ambda表達式,我們可以用它們來設(shè)計可擴展的策略。我們不用非得創(chuàng)建新的接口或者都遵循同一個方法名,可以更聚焦要實現(xiàn)的業(yè)務(wù)邏輯,我們會在73頁的使用lambda表達式進行裝飾中提到。

  輕松實現(xiàn)并發(fā)

  一個大型應(yīng)用快到了發(fā)布里程碑的時候,突然一個嚴重的性能問題浮出水面。團隊迅速定位出性能瓶頸點是出在一個處理海量數(shù)據(jù)的龐大的模塊里。團隊中有人建議說如果能充分發(fā)掘多核的優(yōu)勢的話可以提高系統(tǒng)性能。不過如果這個龐大的模塊是用老的Java風(fēng)格寫的話,剛才這個建議帶來的喜悅很快就破滅了。

  團隊很快意識到要這把這個龐然大物從串行執(zhí)行改成并行需要費很大的精力,增加了額外的復(fù)雜度,還容易引起多線程相關(guān)的BUG。難道沒有一種提高性能的更好方式嗎?

  有沒有可能串行和并行的代碼都是一樣的,不管選擇串行還是并行執(zhí)行,就像按一下開關(guān),表明一下想法就可以了?

  聽起來好像只有納尼亞里面能這樣,不過如果我們完全用函數(shù)式進行開發(fā)的話,這一切都將成為現(xiàn)實。內(nèi)置的迭代器和函數(shù)式風(fēng)格將掃清通往并行化的最后一道障礙。JDK的設(shè)計使得串行和并行執(zhí)行的切換只需要一點不起眼的代碼改動就可以實現(xiàn),我們將會在145頁《完成并行化的飛躍》中提到。

  講故事

  在業(yè)務(wù)需求變成代碼實現(xiàn)的過程中會丟失大量的東西。丟失的越多,出錯的可能性和管理的成本就越高。如果代碼看起來就跟描述需求一樣,將會很方便閱讀,和需求人員討論也變的更簡單,也更容易滿足他們的需求。

  比如你聽到產(chǎn)品經(jīng)理在說,”拿到所有股票的價格,找出價格大于500塊的,計算出能分紅的資產(chǎn)總和”。使用Java提供的新設(shè)施,可以這么寫:

  復(fù)制代碼 代碼如下:

  tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()

  這個轉(zhuǎn)化過程幾乎是無損的,因為基本上也沒什么要轉(zhuǎn)化的。這是函數(shù)式在發(fā)揮作用,在本書中還會看到更多這樣的例子,尤其是第8章,使用lambda表達式來構(gòu)建程序,137頁。

  關(guān)注隔離

  在系統(tǒng)開發(fā)中,核心業(yè)務(wù)和它所需要的細粒度邏輯通常需要進行隔離。比如說,一個訂單處理系統(tǒng)想要對不同的交易來源使用不同的計稅策略。把計稅和其余的處理邏輯進行隔離會使得代碼重用性和擴展性更高。

  在面向?qū)ο缶幊讨形覀儼堰@個稱之為關(guān)注隔離,通常用策略模式來解決這個問題。解決方法一般就是創(chuàng)建一些接口和實現(xiàn)類。

  我們可以用更少的代碼來完成同樣的效果。我們還可以快速嘗試自己的產(chǎn)品思路,而不用上來就得搞出一堆代碼,停滯不前。我們將在63頁的,使用lambda表達式進行關(guān)注隔離中進一步探討如果通過輕量級函數(shù)來創(chuàng)建這種模式以及進行關(guān)注隔離。

  惰性求值

  開發(fā)企業(yè)級應(yīng)用時,我們可能會與WEB服務(wù)進行交互,調(diào)用數(shù)據(jù)庫,處理XML等等。我們要執(zhí)行的操作有很多,不過并不是所有時候都全部需要。避免某些操作或者至少延遲一些暫時不需要的操作是提高性能或者減少程序啟動,響應(yīng)時間的一個最簡單的方式。

  這只是個小事,但用純OOP的方式來實現(xiàn)還需要費一番工夫。為了延遲一些重量級對象的初始化,我們要處理各種對象引用 ,檢查空指針等等。

  不過,如果使用了新的Optinal類和它提供的一些函數(shù)式風(fēng)格的API,這個過程將變得很簡單,代碼也更清晰明了,我們會在105頁的延遲初始化中討論這個。

  提高可測性

  代碼的處理邏輯越少,容易被改錯的地方當然也越少。一般來說函數(shù)式的代碼比較容易修改,測試起來也較簡單。

  另外,就像第4章,使用lambda表達式進行設(shè)計和第5章資源的使用中那樣,lambda表達式可以作為一種輕量級的mock對象,讓異常測試變得更清晰易懂。lambda表達式還可以作為一個很好的測試輔助工具。很多常見的測試用例都可以接受并處理lambda表達式。這樣寫的測試用例能夠抓住需要回歸測試的功能的本質(zhì)。同時,需要測試的各種實現(xiàn)都可以通過傳入不同的lambda表達式來完成。

  JDK自己的自動化測試用例也是lambda表達式的一個很好的應(yīng)用范例——想了解更多的話可以看下OpenJDK倉庫里的源代碼。通過這些測試程序可以看到lambda表達式是如何將測試用例的關(guān)鍵行為進行參數(shù)化;比如,它們是這樣構(gòu)建測試程序的,“新建一個結(jié)果的容器”,然后“對一些參數(shù)化的后置條件進行檢查”。

  我們已經(jīng)看到,函數(shù)式編程不僅能讓我們寫出高質(zhì)量的代碼,還能優(yōu)雅的解決開發(fā)過程中的各種難題。這就是說,開發(fā)程序?qū)⒆兊酶旄唵,出錯也更少——只要你能遵守我們后面將要介紹到的幾條準則。

  第四節(jié):進化而非革命

  我們用不著轉(zhuǎn)向別的語言,就能享受函數(shù)式編程帶來的好處;需要改變的只是使用Java的一些方式。C++,Java,C#這些語言都支持命令式和面向?qū)ο蟮木幊。不過現(xiàn)在它們都開始投入函數(shù)式編程的懷抱里了。我們剛才已經(jīng)看到了這兩種風(fēng)格的代碼,并討論了函數(shù)式編程能帶來的好處,F(xiàn)在我們來看下它的一些關(guān)鍵概念和例子來幫助我們學(xué)習(xí)這種新的風(fēng)格。

  Java語言的開發(fā)團隊花費了大量的時間和精力把函數(shù)式編程的能力添加到了Java語言和JDK里。要享受它帶來的好處,我們得先介紹幾個新的概念。我們只要遵循下面幾條規(guī)則就能提升我們的代碼質(zhì)量:

  1.聲明式

  2.提倡不可變性

  3.避免副作用

  4.優(yōu)先使用表達式而不是語句

  5.使用高階函數(shù)進行設(shè)計

  我們來看下這幾條實踐準則。

  聲明式

  我們所熟悉的命令式編程的核心就是可變性和命令驅(qū)動的編程。我們創(chuàng)建變量,然后不斷修改它們的值。我們還提供了要執(zhí)行的詳細的指令,比如生成迭代的索引標志,增加它的值,檢查循環(huán)是否結(jié)束,更新數(shù)組的第N個元素等。在過去由于工具的特性和硬件的限制,我們只能這么寫代碼。 我們也看到了,在一個不可變集合上,聲明式的contains方法比命令式的更容易使用。所有的難題和低級的操作都在庫函數(shù)里來實現(xiàn)了,我們不用再關(guān)心這些細節(jié)。就沖著簡單這點,我們也應(yīng)該使用聲明式編程。不可變性和聲明式編程是函數(shù)式編程的精髓,現(xiàn)在Java終于把它變成了現(xiàn)實。

  提倡不可變性

  變量可變的代碼會有很多活動路徑。改的東西越多,越容易破壞原有的結(jié)構(gòu),并引入更多的錯誤。有多個變量被修改的代碼難于理解也很難進行并行化。不可變性從根本上消除了這些煩惱。 Java支持不可變性但沒有強制要求——但我們可以。我們需要改變修改對象狀態(tài)這個舊習(xí)慣。我們要盡可能的使用不可變的對象。 聲明變量,成員和參數(shù)的時候,盡量聲明為final的,就像Joshua Bloch在” Effective Java“里說的那句名言那樣,“把對象當成不可變的吧”。 當創(chuàng)建對象的時候,盡量創(chuàng)建不可變的對象,比如String這樣的。創(chuàng)建集合的時候,也盡量創(chuàng)建不可變或者無法修改的集合,比如用Arrays.asList()和Collections的unmodifiableList()這樣的方法。 避免了可變性我們才可以寫出純粹的函數(shù)——也就是,沒有副作用的函數(shù)。

  避免副作用

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

  優(yōu)先使用表達式

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

  使用高階函數(shù)進行設(shè)計

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

  1.把函數(shù)傳給函數(shù)

  2.在函數(shù)內(nèi)創(chuàng)建新的函數(shù)

  3.在函數(shù)內(nèi)返回函數(shù)

  我們已經(jīng)見過一個把函數(shù)傳參給另一個函數(shù)的例子了,在后面我們還會看到創(chuàng)建函數(shù)和返回函數(shù)的示例。我們先再看一遍“把函數(shù)傳參給函數(shù)”的那個例子:

  復(fù)制代碼 代碼如下:

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

  在這段代碼中我們把函數(shù)price -> price.multiply(BigDecimal.valueOf(0.9)),傳給了map函數(shù)。傳遞的這個函數(shù)是在調(diào)用高階函數(shù)map的時候才創(chuàng)建的。通常來說一個函數(shù)有函數(shù)體,函數(shù)名,參數(shù)列表,返回值。這個實時創(chuàng)建的函數(shù)有一個參數(shù)列表后面跟著一個箭頭(->),然后就是很短的一段函數(shù)體了。參數(shù)的類型由Java編譯器來進行推導(dǎo),返回的類型也是隱式的。這是個匿名函數(shù),它沒有名字。不過我們不叫它匿名函數(shù),我們稱之為lambda表達式。 匿名函數(shù)作為傳參在Java并不算是什么新鮮事;我們之前也經(jīng)常傳遞匿名內(nèi)部類。即使匿名類只有一個方法,我們還是得走一遍創(chuàng)建類的儀式,然后對它進行實例化。有了lambda表達式我們可以享受輕量級的語法了。不僅如此,我們之前總是習(xí)慣把一些概念抽象成各種對象,現(xiàn)在我們可以將一些行為抽象成lambda表達式了。 用這種編碼風(fēng)格進行程序設(shè)計還是需要費些腦筋的。我們得把已經(jīng)根深蒂固的命令式思維轉(zhuǎn)變成函數(shù)式的。開始的時候可能有點痛苦,不過很快你就會習(xí)慣它了,隨著不斷的深入,那些非函數(shù)式的API逐漸就被拋到腦后了。 這個話題就先到這吧,我們來看看Java是如何處理lambda表達式的。我們之前總是把對象傳給方法,現(xiàn)在我們可以把函數(shù)存儲起來并傳遞它們。 我們來看下Java能夠?qū)⒑瘮?shù)作為參數(shù)背后的秘密。

  第五節(jié):加了點語法糖

  用Java原有的功能也是可以實現(xiàn)這些的,不過lambda表達式加了點語法糖,省掉了一些步驟,使我們的工作更簡單了。這樣寫出的代碼不僅開發(fā)更快,也更能表達我們的想法。 過去我們用的很多接口都只有一個方法:像Runnable, Callable等等。這些接口在JDK庫中隨處可見,使用它們的地方通常用一個函數(shù)就能搞定。原來的這些只需要一個單方法接口的庫函數(shù)現(xiàn)在可以傳遞輕量級函數(shù)了,多虧了這個通過函數(shù)式接口提供的語法糖。 函數(shù)式接口是只有一個抽象方法的接口。再看下那些只有一個方法的接口,Runnable,Callable等,都適用這個定義。JDK8里面有更多這類的接口——Function, Predicate, Comsumer, Supplier等(157頁,附錄1有更詳細的接口列表)。函數(shù)式接口可以有多個static方法,和default方法,這些方法是在接口里面實現(xiàn)的。 我們可以用@FunctionalInterface注解來標注一個函數(shù)式接口。編譯器不使用這個注解,不過有了它可以更明確的標識這個接口的類型。不止如此,如果我們用這個注解標注了一個接口,編譯器會強制校驗它是否符合函數(shù)式接口的規(guī)則。 如果一個方法接收函數(shù)式接口作為參數(shù),我們可以傳遞的參數(shù)包括:

  1.匿名內(nèi)部類,最古老的方式

  2.lambda表達式,就像我們在map方法里那樣

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

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

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

  總結(jié)

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

【java中l(wèi)ambda表達式】相關(guān)文章:

Java中定義與聲明的區(qū)別03-19

淺談Java中的弱引用04-03

Java中hashmap和hashtable的區(qū)別03-30

Java中的多態(tài)用法實例分析04-04

JAVA中STRING的常用方法總結(jié)04-04

java中String和StringBuffer的區(qū)別03-18

理解java中的關(guān)鍵字04-02

淺談理解Java中的弱引用04-02

Java編程中異常處理的方法12-16