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