前面我們已經(jīng)用過(guò)幾次collect()方法來(lái)將Stream返回的元素拼成ArrayList了。這是一個(gè)reduce操作,它對(duì)于將一個(gè)集合轉(zhuǎn)化成另一種類型(通常是一個(gè)可變的集合)非常有用。collect()函數(shù),如果和Collectors工具類里的一些方法結(jié)合起來(lái)使用的話,能提供極大的便利性,本節(jié)我們將會(huì)介紹到。
我們還是繼續(xù)使用前面的Person列表作為例子,來(lái)看一下collect()方法到底有哪些能耐。假設(shè)我們要從原始列表中找出所有大于20歲的人。下面是使用了可變性和forEach()方法實(shí)現(xiàn)的版本:
我們使用filter()方法來(lái)從列表中過(guò)濾出了所有年齡大于20的人。然后,在forEach方法里,我們將元素添加到一個(gè)在前面已經(jīng)初始化好的ArrayList中。我們先看下這段代碼的輸出結(jié)果,一會(huì)兒再去重構(gòu)它。
程序輸出的結(jié)果是對(duì)的,不過(guò)還有點(diǎn)小問(wèn)題。首先,把元素添加到集合中,這種屬于低級(jí)操作――它是命令式的,而非聲明式的。如果我們想把這個(gè)迭代改造成并發(fā)的,還得去考慮線程安全的問(wèn)題――可變性使得它難以并行化。幸運(yùn)的是,使用collect()方法可以很容易解決掉這個(gè)問(wèn)題。來(lái)看下如何實(shí)現(xiàn)的。
collect()方法接受一個(gè)Stream并將它們收集到一個(gè)結(jié)果容器中。要完成這個(gè)工作,它需要知道三個(gè)東西:
+如何創(chuàng)建結(jié)果容器(比如說(shuō),使用ArrayList::new方法) +如何把單個(gè)元素添加到容器中(比如使用ArrayList::add方法) +如何把一個(gè)結(jié)果集合并到另一個(gè)中(比如使用ArrayList::addAll方法)
對(duì)于串行操作而言,最后一條不是必需的;代碼設(shè)計(jì)的目標(biāo)是能同時(shí)支持串行和并行的。
我們把這些操作提供給collect方法,讓它來(lái)把過(guò)濾后的流給收集起來(lái)。
這段代碼的結(jié)果和前面一樣,不過(guò)這樣寫有諸多好處。
首先,我們編程的方式更聚焦了,表述性也更強(qiáng),清晰的傳達(dá)了你要把結(jié)果收集到一個(gè)ArrayList里去的目的。collect()的第一個(gè)參數(shù)是一工廠或者生產(chǎn)者,后面的參數(shù)是一個(gè)用來(lái)收集元素的操作。
第二,由于我們沒(méi)有在代碼中個(gè)執(zhí)行顯式的修改操作,可以很容易并行地執(zhí)行這個(gè)迭代。我們讓底層庫(kù)來(lái)完成修改操作,它自己會(huì)處理好協(xié)作及線程安全的問(wèn)題,盡管ArrayList本身不是線程安全的――干的漂亮。
如果條件允許的話,collect()方法可以并行地將元素添加到不同的子列表中,然后再用一個(gè)線程安全的方式將它們合并到一個(gè)大列表里(最后一個(gè)參數(shù)就是用來(lái)進(jìn)行合并操作的)。
我們已經(jīng)看到,相對(duì)于手動(dòng)把元素添加到列表而言,使用collect()方法的好處真是太多了。下面我們來(lái)看下這個(gè)方法的一個(gè)重載的版本――它更簡(jiǎn)單也更方便――它是使用一個(gè)Collector作為參數(shù)。這個(gè)Collector是一個(gè)包含了生產(chǎn)者,添加器,以及合并器在內(nèi)的接口――在前面的版本中這些操作是作為獨(dú)立的參數(shù)分別傳入方法中的――使用Collector則更簡(jiǎn)單并且可以復(fù)用。Collectors工具類提供了一個(gè)toList方法,可以生成一個(gè)Collector的實(shí)現(xiàn),用來(lái)把元素添加到ArrayList中。我們來(lái)修改下前面那段代碼,使用一下這個(gè)collect()方法。
使用了Collectors工具類的簡(jiǎn)潔版的collect()方法,可不止這一種用法。Collectors工具類中還有好幾種不同的方法來(lái)可以進(jìn)行不同的收集和添加的操作。比如說(shuō),除了toList()方法,還有toSet()方法,可以添加到一個(gè)Set中,toMap()方法可以用來(lái)收集到一個(gè)key-value的集合中,還有joining()方法,可以拼接成一個(gè)字符串。我們還可以將mapping(),collectingAndThen(),minBy(), maxBy()和groupingBy()等方法組合起來(lái)進(jìn)行使用。
我們來(lái)用下groupingBy()方法來(lái)將人群按年齡進(jìn)行分組。
只需簡(jiǎn)單的調(diào)用下collect()方法便能完成分組。groupingBy()接受一個(gè)lambda表達(dá)式或者方法引用――這種叫分類函數(shù)――它返回需要分組的對(duì)象的某個(gè)屬性的值。根據(jù)我們這個(gè)函數(shù)返回的值,來(lái)把調(diào)用上下文中的元素放進(jìn)某個(gè)分組中。在輸出中可以看到分組的結(jié)果:
這些人已經(jīng)按年齡進(jìn)行了分組。
在前面這個(gè)例子中我們按人群的年齡對(duì)他們進(jìn)行了分組收集。groupingBy()方法的一個(gè)變種可以按多個(gè)條件進(jìn)行分組。簡(jiǎn)單的groupingBy()方法使用了分類器進(jìn)行元素收集。而通用的groupingBy()收集器,則可以為每一個(gè)分組指定一個(gè)收集器。也就是說(shuō),元素在收集的過(guò)程中會(huì)途經(jīng)不同的分類器和集合,下面我們將會(huì)看到。
繼續(xù)使用上面這個(gè)例子,這回我們不按年齡分組了,我們只獲取人的名字,按他們的年齡進(jìn)行排序。
這個(gè)版本的groupingBy()接受兩個(gè)參數(shù):第一個(gè)是年齡,這是分組的條件,第二個(gè)是一個(gè)收集器,它是由mapping()函數(shù)返回的結(jié)果。這些方法都來(lái)自Collectors工具類,在這段代碼中進(jìn)行了靜態(tài)的導(dǎo)入。mapping()方法接受兩個(gè)參數(shù),一個(gè)是映射用的屬性,一個(gè)是對(duì)象要收集到的地方,比如說(shuō)list或者set。來(lái)看下上面這段代碼的輸出結(jié)果:
可以看到,人們的名字已經(jīng)按年齡進(jìn)行分組了。
我們?cè)賮?lái)看一個(gè)組合的操作:按名字的首字母進(jìn)行分組,然后選出每個(gè)分組中年紀(jì)最大的那位。
我們先是按名字的首字母進(jìn)行了排序。為了實(shí)現(xiàn)這個(gè),我們把一個(gè)lambda表達(dá)式作為groupingBy()的第一個(gè)參數(shù)傳了進(jìn)去。這個(gè)lambda表達(dá)式是用來(lái)返回名字的首字母的,以便進(jìn)行分組。第二個(gè)參數(shù)不再是mapping()了,而是執(zhí)行了一個(gè)reduce操作。在每個(gè)分組內(nèi),它使用maxBy()方法,從所有元素中遞推出最年長(zhǎng)的那位。由于組合了許多操作,這個(gè)語(yǔ)法看起來(lái)有點(diǎn)臃腫,不過(guò)整個(gè)讀起來(lái)是這樣的:按名字首字母進(jìn)行分組,然后遞推出分組中最年長(zhǎng)的那位。來(lái)看下這段代碼的輸出,它列出了指定字母開(kāi)頭的那組名字中年紀(jì)最大的那個(gè)人。
我們已經(jīng)領(lǐng)教到了collect()方法以及Collectors工具類的威力。在你的IDE或者JDK的官方文檔里面,再花點(diǎn)時(shí)間去研究下Collectors工具類吧,熟悉下它提供的各種方法。下面我們將會(huì)用lambda表達(dá)式來(lái)完成一些過(guò)濾器的實(shí)現(xiàn)。
新聞熱點(diǎn)
疑難解答
圖片精選