收集器簡介 Collector
函數式編程相對于指令式編程的一個主要優勢:你只需要指出希望的結果“做什么”,而不用操心執行的步驟“如何做”。
收集器用作高級規約
函數式API設計的另一個好處:更容易復合和重用。收集器非常有用,因為用它可以簡潔而靈活地定義collect用來生成結果集合的標準。更具體的說,對流調用collect方法將對流中的元素觸發一個規約操作(由Coolector來參數化)。
List<Transaction> transactions = transactionStream.collect(Collectors.toList());
規約和匯總
利用counting工廠方法返回收集器,數一數菜單里有多少種菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
還可以這樣寫更為直接:
long howManyDishes = menu.stream().count();
查找流中的最大值和最小值 maxBy minBy
找出菜單中熱量最高的菜。你可以使用兩個收集器,Collectors.maxBy()和Collectors.minBy()
你可以創建一個Comparator來根據所含熱量對菜肴進行比較,并把Collectors.maxBy:
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy((a1,a2)—>a1.getCalories - a2.getCalories));
匯總 summingInt summingDouble
Collectors類專門為匯總提供了一個工廠方法:Collectors.summintInt。它可接受一個把對象映射為求和所需int的函數,并返回一個收集器;該收集器在傳遞給普通的collect方法后即執行我們需要的匯總操作。舉個例子,計算菜單列表的總熱量:
int totalCalories = menu.stream().collect(summingInt((v)-> v.getCalories()));
匯總不僅僅求和;還有Collectors.averageingInt,連同對應的averagingInt等計算數值的平均數。
int averageCalories = menu.stream().collect(averagintInt((a) -> a.getCalories()));
到目前為止,已經看到了如何使用手機器來給流中的元素計數,找到這些元素數值屬性的最大值和最小值,以及計算其總和和平均值。不過很多時候,你可能想要得到兩個或更多這樣的結果,而且你希望只需一次操作就可以完成。這種情況下,你可以就輸出菜單中的元素個數,并得到菜肴熱量的總和,平均值,最大值和最小值。
IntSummaryStatastics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
這個收集器會把所有這些信息收集到一個叫做IntSummaryStatistics的類里面,他提供了方便的取值(getter)方法來訪問結果。打印menuStatisticobject會得到下面的結果:
IntSummaryStatistics{count=9,sum=4300,min=120,average=47.7778,max=800}
連接成字符串
joining工廠方法返回的收集器會把對流中每一個對象應用toString方法得到的所有字符串連接成一個字符串。這意味著你把菜單中所有的菜肴的名稱連接起來。
String shortMenu = menu.stream().map(Dish:getName).collect(joining());
請注意,joining在內部使用了StringBuilder來把生成的字符串逐個追加起來。此外還要注意,如果Dish類有一個toString方法來返回菜肴的名稱,那么你無需提取每一道菜名稱的函數來對原流做映射就可以得到相同的結果:
String shortMenu = menu.stream().collect(jioning());
但該字符串的可讀性并不好。幸好,joining的工廠方法有一個重載版本可以接受元素之間的分解符,這樣你就可以得到一個逗號分隔符的菜肴名稱列表:
String shortMenu = menu.stream().map(Dish::getname()).collect(joining(", "));
到目前為止,我們已經探討了各種將流歸約到一個值的收集器。在下一節中,我們會展示為什么所有這種形式的歸約過程,其實都是Collectors.reducing工廠方法提供的更廣義歸約收集器的特殊的情況。
廣義的歸約匯總
事實上,我們已經討論的所有的收集器,都是一個可以用reducing的工廠方法定義的歸約過程的特殊情況二期。Collectors.reducing工廠方法是所有這些特殊情況的一般化。列如,可以用reducing方法創建的收集器來計算你菜單的總熱量,如下:
int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));
同樣,你可以使用下面這樣單參數形式的reducing來找到熱量最高的菜,如下所示:
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1,d2)_.d1.getCalories()>d2.getCalories()?d1:d2));
分組
一個常見的數據庫操作是根據一個或多個屬性對集合中的項目進行分組。就像按貨幣對交易進行分組,如果用指令式風格來實現的話,這個操作可能會很麻煩,啰嗦,容易出錯。但是用java8的話很容易看懂。舉一個列子:假設要把菜單中的菜按照類型進行分類,有肉的放一組,有魚的放一組,其他的放一組。用Collectors.groupingBy工廠方法返回的收集器就可以輕松的完成這項任務。
Map<Dish.Type,List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
這里,你給groupingBy方法傳遞了一個Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我們把這個Function叫做分類函數,因為它用來把流中的元素分成不同的組。分組結果時一個Map,把分組函數返回的值座位映射的建,把流中所有具有這個分類值的項目的列表座位對應的映射值。在菜單分裂的例子中,鍵就是菜的類型,值就是包含所有對應類型的菜肴列表。
多級分組
要實現多級分組,我們可以使用一個右雙參數版本的Collectors.groupingBy工廠方法創建的收集器,它除了普通的分類函數之外,還可以接受collector類型的第二個參數。那么要進行二級分組的話,我們可以吧一個內層groupingBy傳遞給外層groupingBy,并定義一個為流中項目分類的二級標準。
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})));
分區
分區是分組的特殊情況:由一個謂詞(返回一個布爾值的函數)作為分類函數,它稱分區函數。分區函數返回一個布爾值,這意味著得到的分組Map的鍵類型是Boolean,于是它最多可以分為兩組——true是一組,false是一組。例如,如果你是素食這或是請了一位素食的朋友來共進晚餐,可能會想要把菜單按照素食和非素食分開:
Map<Boolean,List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
那么通過Map中鍵位true的值,就可以找出所有的素食菜肴了:
List<Dish> vegetarianDishes = partitionedMenu.get(true);
收集器接口 Collector
Collector接口包含了一系列方法,為實現具體的歸約操作提供了范本。我們已經看過了Collector接口中實現的許多收集器,列如toList或者groupingBy.這也意味著,你可以為Collector接口提供自己的實現,從而自由地創建自定義歸約操作。
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 :是累加器的類型,累加器是在收集過程中用于累積部分結果的對象。
R :是手機操作得到的對象(通常但并不一定是集合)的類型。
例如,你可以實現一個ToListCollector<T> 類,將Stream<T>中的所有元素收集到一個List<T>里,它的簽名如下:
public class ToListCollector<T> implements Collector<T,List<T>,List<T>>
理解Collector接口聲明的方法
現在我們可以一個一個來分析Collector接口聲明的五個方法了。通過分析,你會注意到,前四個方法都會返回一個會被collect方法調用的函數,而第五個方法characteristics則提供了一系列特征,也就是一個提示列表,告訴collect方法在執行歸約操作的時候可以應用那些優化(比如并行化)
建立新的結果容器:supplier方法
supplier方法必須返回一個結果為空Suppier,也就是一個無參數函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用。很明顯,對于將累加器本身作為結果返回的收集器,比如我們的ToListCollector,在對空流執行操作的時候,這個空的累加器也代表了收集過程的結果。在我們的ToListCollector中,supplier返回一個空的List
public Supplier<List<T>> supplier(){
return ()->new ArrayList<T>();
}
請注意你也可以值傳遞一個構造函數引用:
public Supplier<List<T>> supplier(){
return ArrayList::new;
}
將元素添加到結果容器:accumulator方法
accumulator方法會返回執行歸約操作的函數。當遍歷到流中的第n個元素是,這個函數執行時會有兩個參數:保存歸約結果的累加器(已經收集了流中的前n-1個項目),還有第n個元素本身。該函數將返回void,因為累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷的元素效果。對于ToListCollector,這個函數僅僅會把當前項目添加至已經遍歷過的項目的列表:
public BiConsumer<List<T>,T> accumulator(){
return (list,item) -> list.add(item);
}
也可以使用方法引用,這會更簡潔:
public BiConsumer<List<T>,T> accumulator(){
return List::add;
}
對結果容器應用最終轉化:finisher方法
在遍歷完流后,finisher方法必須返回在累積過程的最后要調用一個函數,以便將累加器對象轉換為整個集合操作的最終效果。通常,就像ToListCollector的情況一些樣,累加器對象恰好復合預期的最終效果,因此無需轉換。所以finisher方法只需返回identity函數:
public Funciton<List<T>,List<T>> finisher(){
reutrn Function.identity();
}
合并兩個結果容器:combiner方法
四個方法中的最后一個——combiner方法會返回一個供歸約操作使用的函數,它定義了對流的各個子部分進行并行處理時,各個子部分歸約所得的累加器要如何合并。對于toList而言,這個方法的實現非常簡單,只要把從流的第二部分收集到的項目列表加到遍歷第一部分時得到的列表后面就行了:
public BinaryOperator<List<T>> combiner(){
return (list1,list2)->{
list1.addAll(list2);
return list1;
}
}
- 原始流會以遞歸方式拆分為子流,直到定義流是否需要進一步拆分的一個條件為非(如果分布式工作單位太小,并行計算往往比順序計算要慢,而且要是生成的并行任務比處理器內核書多的話就毫無意義了)
- 現在,所有的子流都可以并行處理,即對每個子流應用上圖的順序歸約算法
- 最后,使用收集器combiner方法返回函數,將所有的部分結果兩兩合并。這時會把原始流每次拆分時得到的子流對應的結果和并起來。
characteristics方法
最后一個方法——characteriestics會返回一個不可表你的Characteristics集合,它定義了收集器的行為——尤其是關于可以并行歸約,以及可以使用那些優化的提示。Characteristics是一個包含三個項目的枚舉。
- UNORDERED——歸約結果不受流中項目的遍歷和累積順序的影響。
- CONCURRENT——accumulator函數可以從多個線程同時調用,且該收集器可以并行歸約流。如果收集器沒有表為UNORDERED,那它盡在用于無需數據源時才可以并行歸約。
- IDENTITY_FINISH——這表明完成器方法返回的函數是一個恒等函數,可以跳過。這種情況下,累加器對象將會直接用作歸約過程的最終結果。這也意味著,將累加器A不加檢查的轉化為結果R是安全的。
ToListCollector是IDENTITY_FINISH的,因為用來累積流中元素的List已經是我們要的最終結果,用不著進一步轉換了,但他并不是UNORDERED,因為用在有序留上的時候,我們還是希望順序能夠保留在得到的List中。最后,他是CONCURRENT的,但我們剛才說過了,僅僅在背后的數據源無序時才會進行并行處理