Java函數(shù)式編程(二):集合的使用
第二章:集合的使用
我們經(jīng)常會用到各種集合,數(shù)字的,字符串的還有對象的。它們無處不在,哪怕操作集合的代碼要能稍微優(yōu)化一點(diǎn),都能讓代碼清晰很多。在這章中,我們探索下如何使用lambda表達(dá)式來操作集合。我們用它來遍歷集合,把集合轉(zhuǎn)化成新的集合,從集合中刪除元素,把集合進(jìn)行合并。
遍歷列表
遍歷列表是最基本的一個集合操作,這么多年來,它的操作也發(fā)生了一些變化。我們使用一個遍歷名字的小例子,從最古老的版本介紹到現(xiàn)在最優(yōu)雅的版本。
用下面的代碼我們很容易創(chuàng)建一個不可變的名字的列表:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(friends.get(i));
}
下面這是最常見的一種遍歷列表并打印的方法,雖然也最一般:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
我把這種方式叫做自虐型寫法——又啰嗦又容易出錯。我們得停下來好好想想,"是i<還是i<=呢?"這只有當(dāng)我們需要操作具體某個元素的時候才有意義,不過即便這樣,我們還可以使用堅持不可變原則的函數(shù)式風(fēng)格來實(shí)現(xiàn),這個我們很快會討論到。
Java還提供了一種相對先進(jìn)的for結(jié)構(gòu)。
collections/fpij/Iteration.java
for(String name : friends) {
System.out.println(name);
}
在底層,這種方式的迭代是使用Iterator接口來實(shí)現(xiàn)的,調(diào)用了它的hasNext和next方法。 這兩種方式都屬于外部迭代器,它們把如何做和想做什么揉到了一起。我們顯式的控制迭代,告訴它從哪開始到哪結(jié)束;第二個版本則在底層通過Iterator的方法來做這些。顯式的操作下,還可以用break和continue語句來控制迭代。 第二個版本比第一個少了點(diǎn)東西。如果我們不打算修改集合的某個元素的話,它的方式比第一個要好。不過這兩種方式都是命令式的,在現(xiàn)在的Java中應(yīng)該摒棄這種方式。 改成函數(shù)式原因有這幾個:
1.for循環(huán)本身是串行的,很難進(jìn)行并行化。
2.這樣的循環(huán)是非多態(tài)的;所得即所求。我們直接把集合傳給for循環(huán),而不是在集合上調(diào)用一個方法(支持多態(tài))來執(zhí)行特定的操作。
3.從設(shè)計層面來說,這樣 寫的代碼違反了“Tell,Don't Ask”的原則 。我們請求執(zhí)行一次迭代,而不是把迭代留給底層庫來執(zhí)行。
是時候從老的命令式編程轉(zhuǎn)換到更優(yōu)雅的內(nèi)部迭代器的函數(shù)式編程了。使用內(nèi)部迭代器后我們把很多具體操作都扔給了底層方法庫來執(zhí)行,你可以更專注于具體的業(yè)務(wù)需求。底層的函數(shù)會負(fù)責(zé)進(jìn)行迭代的。我們先用一個內(nèi)部迭代器來枚舉一下名字列表。
Iterable接口在JDK8中得到加強(qiáng),它有一個專門的名字叫forEach,它接收一個Comsumer類型的參數(shù)。如名字所說,Consumer的實(shí)例正是通過它的accept方法消費(fèi)傳遞給它的對象的。我們用一個很熟悉的匿名內(nèi)部類的語法來使用下這個forEach方法:
friends.forEach(new Consumer<String>() { public void accept(final String name) {
System.out.println(name); }
});
我們調(diào)用了friends集合上的forEach方法,給它傳遞了一個Consumer的匿名實(shí)現(xiàn)。這個forEach方法從對集合中的每一個元素調(diào)用傳入的Consumer的accept方法,讓它來處理這個元素。在這個示例中我們只是打印了一下它的值,也就是這個名字。 我們來看下這個版本的輸出結(jié)果,和上兩個的結(jié)果 是一樣的:
Brian
Nate
Neal
Raju
Sara
Scott
我們只改了一個地方:我們拋棄了過時的 for循環(huán),使用了新的內(nèi)部迭代器。好處是,我們不用指定如何迭代這個集合,可以更專注于如何處理每一個元素。缺點(diǎn)是,代碼看起來更啰嗦了——這簡直要把新的編碼風(fēng)格帶來的喜悅沖的一干二凈了。所幸的是,這個很容易改掉,這正是lambda表達(dá)式和新的編譯器的威力大展身手的時候了。我們再做一點(diǎn)修改,把匿名內(nèi)部類換成lambda表達(dá)式。
friends.forEach((final String name) -> System.out.println(name));
這樣看起來就好多了。代碼更少了,不過我們先來看下這是什么意思。這個forEach方法是一個高階函數(shù),它接收一個lambda表達(dá)式或者代碼塊,來對列表中的元素進(jìn)行操作。在每次調(diào)用的時候 ,集合中的元素會綁定到name這個變量上。底層庫托管了lambda表達(dá)式調(diào)用的活。它可以決定延遲表達(dá)式的執(zhí)行,如果合適的話還可以進(jìn)行并行計算。 這個版本的輸出也和前面的一樣。
Brian
Nate
Neal
Raju
Sara
Scott
內(nèi)部迭代器的版本更為簡潔。而且,使用它的話我們可以更專注每個元素的處理操作,而不是怎么去遍歷——這可是聲明式的。
不過這個版本還有缺陷。一旦forEach方法開始執(zhí)行了,不像別的兩個版本,我們沒法跳出這個迭代。(當(dāng)然有別的方法能搞定這個)。因此,這種寫法在需要對集合里的每個元素處理的時候比較常用。后面我們會介紹到一些別的函數(shù)可以讓我們控制循環(huán)的過程。
lambda表達(dá)式的標(biāo)準(zhǔn)語法,是把參數(shù)放到()里面,提供類型信息并使用逗號分隔參數(shù)。Java編譯器為了解放我們,還能自動進(jìn)行類型推導(dǎo)。不寫類型當(dāng)然更方便了,工作少了,世界也清靜了。下面是上一個版本去掉了參數(shù)類型之后的:
friends.forEach((name) -> System.out.println(name));
在這個例子里,Java編譯器通過上下文分析,知道name的類型是String。它查看被調(diào)用方法forEach的簽名,然后分析參數(shù)里的這個函數(shù)式接口。接著它會分析這個接口里的抽象方法,查看參數(shù)的個數(shù)及類型。即便這個lambda表達(dá)式接收多個參數(shù),我們也一樣能進(jìn)行類型推導(dǎo),不過這樣的話所有參數(shù)都不能帶參數(shù)類型;在lambda表達(dá)式中,參數(shù)類型要么全不寫,要寫的話就得全寫。
Java編譯器對單個參數(shù)的lambda表達(dá)式會進(jìn)行特殊處理:如果你想進(jìn)行類型推導(dǎo)的話,參數(shù)兩邊的括號可以省略掉。
friends.forEach(name -> System.out.println(name));
這里有一點(diǎn)小警告:進(jìn)行類型推導(dǎo)的參數(shù)不是final類型的。在前面顯式聲明類型例子中,我們同時也把參數(shù)標(biāo)記為final的。這樣能防止你在lambda表達(dá)式中修改參數(shù)的值。通常來說,修改參數(shù)的值是個壞習(xí)慣,這樣容易引起B(yǎng)UG,因此標(biāo)記成final是個好習(xí)慣。不幸的是,如果我們想使用類型推導(dǎo)的話,我們就得自己遵守規(guī)則不要修改參數(shù),因?yàn)榫幾g器可不再為我們保駕護(hù)航了。
走到這步可費(fèi)了老勁了,現(xiàn)在代碼量確實(shí)少了一點(diǎn)。不過這還不算最簡。我們來體驗(yàn)下最后這個極簡版的。
friends.forEach(System.out::println);
在上面的代碼中我們用到了一個方法引用。我們用方法名就可以直接替換整個的代碼了。在下節(jié)中我們會深入探討下這個,不過現(xiàn)在我們先來回憶下Antoine de Saint-Exupéry的一句名言:完美不是無法再增添加什么,而是無法再去掉什么。
lambda表達(dá)式讓我們能夠簡潔明了的進(jìn)行集合的遍歷。下一節(jié)我們會講到它如何使我們在進(jìn)行刪除操作和集合轉(zhuǎn)化的時候,也能夠?qū)懗鋈绱撕啙嵉拇a。
相關(guān)文章
Java 中實(shí)現(xiàn)隨機(jī)無重復(fù)數(shù)字的方法
為了更好地理解這個題意,我們先來看下具體內(nèi)容:生成一個1-100 的隨機(jī)數(shù)組,但數(shù)組中的數(shù)字不能重復(fù),即位置是隨機(jī)的,但數(shù)組元素不能重復(fù)2013-03-03springboot2.x只需兩步快速整合log4j2的方法
這篇文章主要介紹了springboot2.x只需兩步快速整合log4j2的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Java?Chassis3熔斷機(jī)制的改進(jìn)路程技術(shù)解密
這篇文章主要介紹了Java?Chassis?3技術(shù)解密之熔斷機(jī)制的改進(jìn)路程實(shí)例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Maven本地打包war包實(shí)現(xiàn)代碼解析
這篇文章主要介紹了Maven本地打包war包實(shí)現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09