收集器簡介 Collector
函數(shù)式編程相對于指令式編程的一個主要優(yōu)勢:你只需要指出希望的結(jié)果“做什么”,而不用操心執(zhí)行的步驟“如何做”全陨。
收集器用作高級規(guī)約
函數(shù)式API設(shè)計的另一個好處:更容易復(fù)合和重用夺欲。收集器非常有用获询,因為用它可以簡潔而靈活地定義collect用來生成結(jié)果集合的標(biāo)準(zhǔn)方椎。更具體的說,對流調(diào)用collect方法將對流中的元素觸發(fā)一個規(guī)約操作(由Coolector來參數(shù)化)蒂窒。
List<Transaction> transactions = transactionStream.collect(Collectors.toList());
規(guī)約和匯總
利用counting工廠方法返回收集器,數(shù)一數(shù)菜單里有多少種菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
還可以這樣寫更為直接:
long howManyDishes = menu.stream().count();
查找流中的最大值和最小值 maxBy minBy
找出菜單中熱量最高的菜荞怒。你可以使用兩個收集器洒琢,Collectors.maxBy()和Collectors.minBy()
你可以創(chuàng)建一個Comparator來根據(jù)所含熱量對菜肴進(jìn)行比較,并把Collectors.maxBy:
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy((a1,a2)—>a1.getCalories - a2.getCalories));
匯總 summingInt summingDouble
Collectors類專門為匯總提供了一個工廠方法:Collectors.summintInt褐桌。它可接受一個把對象映射為求和所需int的函數(shù)衰抑,并返回一個收集器;該收集器在傳遞給普通的collect方法后即執(zhí)行我們需要的匯總操作荧嵌。舉個例子呛踊,計算菜單列表的總熱量:
int totalCalories = menu.stream().collect(summingInt((v)-> v.getCalories()));
匯總不僅僅求和;還有Collectors.averageingInt,連同對應(yīng)的averagingInt等計算數(shù)值的平均數(shù)。
int averageCalories = menu.stream().collect(averagintInt((a) -> a.getCalories()));
到目前為止啦撮,已經(jīng)看到了如何使用手機(jī)器來給流中的元素計數(shù)谭网,找到這些元素數(shù)值屬性的最大值和最小值,以及計算其總和和平均值赃春。不過很多時候愉择,你可能想要得到兩個或更多這樣的結(jié)果,而且你希望只需一次操作就可以完成织中。這種情況下锥涕,你可以就輸出菜單中的元素個數(shù),并得到菜肴熱量的總和狭吼,平均值层坠,最大值和最小值。
IntSummaryStatastics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
這個收集器會把所有這些信息收集到一個叫做IntSummaryStatistics的類里面搏嗡,他提供了方便的取值(getter)方法來訪問結(jié)果窿春。打印menuStatisticobject會得到下面的結(jié)果:
IntSummaryStatistics{count=9,sum=4300,min=120,average=47.7778,max=800}
連接成字符串
joining工廠方法返回的收集器會把對流中每一個對象應(yīng)用toString方法得到的所有字符串連接成一個字符串拉一。這意味著你把菜單中所有的菜肴的名稱連接起來。
String shortMenu = menu.stream().map(Dish:getName).collect(joining());
請注意旧乞,joining在內(nèi)部使用了StringBuilder來把生成的字符串逐個追加起來蔚润。此外還要注意,如果Dish類有一個toString方法來返回菜肴的名稱尺栖,那么你無需提取每一道菜名稱的函數(shù)來對原流做映射就可以得到相同的結(jié)果:
String shortMenu = menu.stream().collect(jioning());
但該字符串的可讀性并不好嫡纠。幸好,joining的工廠方法有一個重載版本可以接受元素之間的分解符延赌,這樣你就可以得到一個逗號分隔符的菜肴名稱列表:
String shortMenu = menu.stream().map(Dish::getname()).collect(joining(", "));
到目前為止除盏,我們已經(jīng)探討了各種將流歸約到一個值的收集器。在下一節(jié)中挫以,我們會展示為什么所有這種形式的歸約過程者蠕,其實都是Collectors.reducing工廠方法提供的更廣義歸約收集器的特殊的情況。
廣義的歸約匯總
事實上掐松,我們已經(jīng)討論的所有的收集器踱侣,都是一個可以用reducing的工廠方法定義的歸約過程的特殊情況二期。Collectors.reducing工廠方法是所有這些特殊情況的一般化大磺。列如抡句,可以用reducing方法創(chuàng)建的收集器來計算你菜單的總熱量,如下:
int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));
同樣杠愧,你可以使用下面這樣單參數(shù)形式的reducing來找到熱量最高的菜待榔,如下所示:
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1,d2)_.d1.getCalories()>d2.getCalories()?d1:d2));
分組
一個常見的數(shù)據(jù)庫操作是根據(jù)一個或多個屬性對集合中的項目進(jìn)行分組。就像按貨幣對交易進(jìn)行分組流济,如果用指令式風(fēng)格來實現(xiàn)的話锐锣,這個操作可能會很麻煩,啰嗦袭灯,容易出錯刺下。但是用java8的話很容易看懂。舉一個列子:假設(shè)要把菜單中的菜按照類型進(jìn)行分類稽荧,有肉的放一組橘茉,有魚的放一組,其他的放一組姨丈。用Collectors.groupingBy工廠方法返回的收集器就可以輕松的完成這項任務(wù)畅卓。
Map<Dish.Type,List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
這里,你給groupingBy方法傳遞了一個Function(以方法引用的形式)蟋恬,它提取了流中每一道Dish的Dish.Type翁潘。我們把這個Function叫做分類函數(shù),因為它用來把流中的元素分成不同的組歼争。分組結(jié)果時一個Map拜马,把分組函數(shù)返回的值座位映射的建渗勘,把流中所有具有這個分類值的項目的列表座位對應(yīng)的映射值。在菜單分裂的例子中俩莽,鍵就是菜的類型旺坠,值就是包含所有對應(yīng)類型的菜肴列表。
多級分組
要實現(xiàn)多級分組扮超,我們可以使用一個右雙參數(shù)版本的Collectors.groupingBy工廠方法創(chuàng)建的收集器取刃,它除了普通的分類函數(shù)之外,還可以接受collector類型的第二個參數(shù)出刷。那么要進(jìn)行二級分組的話璧疗,我們可以吧一個內(nèi)層groupingBy傳遞給外層groupingBy,并定義一個為流中項目分類的二級標(biāo)準(zhǔn)。
Map<Dish.Type,Map<CalorcLevel,List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(groupingBy(Dish::getType,groupingBy(dish ->{
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT})));
分區(qū)
分區(qū)是分組的特殊情況:由一個謂詞(返回一個布爾值的函數(shù))作為分類函數(shù)馁龟,它稱分區(qū)函數(shù)崩侠。分區(qū)函數(shù)返回一個布爾值,這意味著得到的分組Map的鍵類型是Boolean,于是它最多可以分為兩組——true是一組屁柏,false是一組啦膜。例如,如果你是素食這或是請了一位素食的朋友來共進(jìn)晚餐淌喻,可能會想要把菜單按照素食和非素食分開:
Map<Boolean,List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
那么通過Map中鍵位true的值,就可以找出所有的素食菜肴了:
List<Dish> vegetarianDishes = partitionedMenu.get(true);
收集器接口 Collector
Collector接口包含了一系列方法雀摘,為實現(xiàn)具體的歸約操作提供了范本裸删。我們已經(jīng)看過了Collector接口中實現(xiàn)的許多收集器,列如toList或者groupingBy.這也意味著阵赠,你可以為Collector接口提供自己的實現(xiàn)涯塔,從而自由地創(chuàng)建自定義歸約操作。
Collector接口定義
public interface Collector<T,A,R>{
Suppier<A> supplier();
BiConsumer<A,T> accumulator();
Function<A,R> finisher();
BinaryOperator<A> combiner();
Set<Charactoristics> characteristics();
}
T :是流要收集的項目的泛型清蚀。
A :是累加器的類型匕荸,累加器是在收集過程中用于累積部分結(jié)果的對象。
R :是手機(jī)操作得到的對象(通常但并不一定是集合)的類型枷邪。
例如榛搔,你可以實現(xiàn)一個ToListCollector<T> 類,將Stream<T>中的所有元素收集到一個List<T>里东揣,它的簽名如下:
public class ToListCollector<T> implements Collector<T,List<T>,List<T>>
理解Collector接口聲明的方法
現(xiàn)在我們可以一個一個來分析Collector接口聲明的五個方法了践惑。通過分析,你會注意到嘶卧,前四個方法都會返回一個會被collect方法調(diào)用的函數(shù)尔觉,而第五個方法characteristics則提供了一系列特征,也就是一個提示列表芥吟,告訴collect方法在執(zhí)行歸約操作的時候可以應(yīng)用那些優(yōu)化(比如并行化)
建立新的結(jié)果容器:supplier方法
supplier方法必須返回一個結(jié)果為空Suppier,也就是一個無參數(shù)函數(shù)侦铜,在調(diào)用時它會創(chuàng)建一個空的累加器實例专甩,供數(shù)據(jù)收集過程使用。很明顯钉稍,對于將累加器本身作為結(jié)果返回的收集器涤躲,比如我們的ToListCollector,在對空流執(zhí)行操作的時候嫁盲,這個空的累加器也代表了收集過程的結(jié)果篓叶。在我們的ToListCollector中,supplier返回一個空的List
public Supplier<List<T>> supplier(){
return ()->new ArrayList<T>();
}
請注意你也可以值傳遞一個構(gòu)造函數(shù)引用:
public Supplier<List<T>> supplier(){
return ArrayList::new;
}
將元素添加到結(jié)果容器:accumulator方法
accumulator方法會返回執(zhí)行歸約操作的函數(shù)羞秤。當(dāng)遍歷到流中的第n個元素是缸托,這個函數(shù)執(zhí)行時會有兩個參數(shù):保存歸約結(jié)果的累加器(已經(jīng)收集了流中的前n-1個項目),還有第n個元素本身瘾蛋。該函數(shù)將返回void,因為累加器是原位更新俐镐,即函數(shù)的執(zhí)行改變了它的內(nèi)部狀態(tài)以體現(xiàn)遍歷的元素效果。對于ToListCollector,這個函數(shù)僅僅會把當(dāng)前項目添加至已經(jīng)遍歷過的項目的列表:
public BiConsumer<List<T>,T> accumulator(){
return (list,item) -> list.add(item);
}
也可以使用方法引用哺哼,這會更簡潔:
public BiConsumer<List<T>,T> accumulator(){
return List::add;
}
對結(jié)果容器應(yīng)用最終轉(zhuǎn)化:finisher方法
在遍歷完流后佩抹,finisher方法必須返回在累積過程的最后要調(diào)用一個函數(shù),以便將累加器對象轉(zhuǎn)換為整個集合操作的最終效果取董。通常棍苹,就像ToListCollector的情況一些樣,累加器對象恰好復(fù)合預(yù)期的最終效果茵汰,因此無需轉(zhuǎn)換枢里。所以finisher方法只需返回identity函數(shù):
public Funciton<List<T>,List<T>> finisher(){
reutrn Function.identity();
}
合并兩個結(jié)果容器:combiner方法
四個方法中的最后一個——combiner方法會返回一個供歸約操作使用的函數(shù),它定義了對流的各個子部分進(jìn)行并行處理時蹂午,各個子部分歸約所得的累加器要如何合并栏豺。對于toList而言,這個方法的實現(xiàn)非常簡單豆胸,只要把從流的第二部分收集到的項目列表加到遍歷第一部分時得到的列表后面就行了:
public BinaryOperator<List<T>> combiner(){
return (list1,list2)->{
list1.addAll(list2);
return list1;
}
}
- 原始流會以遞歸方式拆分為子流奥洼,直到定義流是否需要進(jìn)一步拆分的一個條件為非(如果分布式工作單位太小,并行計算往往比順序計算要慢晚胡,而且要是生成的并行任務(wù)比處理器內(nèi)核書多的話就毫無意義了)
- 現(xiàn)在灵奖,所有的子流都可以并行處理,即對每個子流應(yīng)用上圖的順序歸約算法
- 最后搬泥,使用收集器combiner方法返回函數(shù)桑寨,將所有的部分結(jié)果兩兩合并。這時會把原始流每次拆分時得到的子流對應(yīng)的結(jié)果和并起來忿檩。
characteristics方法
最后一個方法——characteriestics會返回一個不可表你的Characteristics集合尉尾,它定義了收集器的行為——尤其是關(guān)于可以并行歸約,以及可以使用那些優(yōu)化的提示燥透。Characteristics是一個包含三個項目的枚舉沙咏。
- UNORDERED——歸約結(jié)果不受流中項目的遍歷和累積順序的影響辨图。
- CONCURRENT——accumulator函數(shù)可以從多個線程同時調(diào)用,且該收集器可以并行歸約流肢藐。如果收集器沒有表為UNORDERED故河,那它盡在用于無需數(shù)據(jù)源時才可以并行歸約。
- IDENTITY_FINISH——這表明完成器方法返回的函數(shù)是一個恒等函數(shù)吆豹,可以跳過鱼的。這種情況下,累加器對象將會直接用作歸約過程的最終結(jié)果痘煤。這也意味著凑阶,將累加器A不加檢查的轉(zhuǎn)化為結(jié)果R是安全的。
ToListCollector是IDENTITY_FINISH的衷快,因為用來累積流中元素的List已經(jīng)是我們要的最終結(jié)果宙橱,用不著進(jìn)一步轉(zhuǎn)換了,但他并不是UNORDERED,因為用在有序留上的時候蘸拔,我們還是希望順序能夠保留在得到的List中师郑。最后,他是CONCURRENT的调窍,但我們剛才說過了宝冕,僅僅在背后的數(shù)據(jù)源無序時才會進(jìn)行并行處理