在前面已經使用過collect終端操作了,主要是用來把Stream中的所有元素結合成一個List,在本章中,你會發現collect是一個歸約操作,就像reduce一樣可以接受各種做法作為參數,將流中的元素累計成一個匯總結果。
看這個例子:按照菜類進行分組
List<Dish> menu = Arrays.asList( new Dish("豬肉燉粉條", false, 800, Type.MEAT), new Dish("小炒牛肉", false, 700, Type.MEAT), new Dish("宮保雞丁", false, 400, Type.MEAT), new Dish("地三鮮", true, 530, Type.OTHER), new Dish("水煮菠菜", true, 350, Type.OTHER), new Dish("拔絲地瓜", true, 120, Type.OTHER), new Dish("火山下雪", true, 550, Type.OTHER), new Dish("水煮魚", false, 330, Type.FISH), new Dish("於是乎", false, 450, Type.FISH) ); //按照類型分組 java 7 Map<Type, List<Dish>> DishsByTypes = new HashMap<>(); for (Dish dish : menu) { Type type = dish.getType(); List<Dish> dishForType = DishsByTypes.get(type); if (dishForType == null) { dishForType = new ArrayList<>(); DishsByTypes.put(type, dishForType); } dishForType.add(dish); }
如果用java 8的話..
Map<Type,List<Dish>> dishs = menu.stream().collect(groupingBy(Dish::getType));
收集器簡介
在上一個例子中,你只需要給出指令“做什么”,而不是編寫實現步驟“如何做”。以前toList()方法只是說按順序給每一個元素生成一個列表。在這個例子中,groupingBy說的是生成一個Map,它的鍵是菜的種類,他們值是那些菜。
1.收集器用作高級歸約
對流調用collect方法將對流中的元素觸發一個歸約操作,它遍歷流中的每一個元素,並讓Collector進行處理。如toList靜態方法,他會把流中的每一個元素收集到一個list中。
2.預定義收集器
Collectors類提供的工廠方法創建的收集器,他們主要提供了三大功能:將流元素歸約和匯總為一個值、元素分組、元素分區。
歸約和匯總
Collectors.counting方法返回的收集器,查看集合數量:
long count1 = menu.stream().collect(counting());
也可以寫為:
long count2 = menu.stream().count();
1.查找流中的最大值和最小值
Collectors.myBy collectors.minBy 返回最大值和最小值,是可空的。
Optional<Dish> max = menu.stream().collect(maxBy(Comparator.comparing(Dish::getCalories))); Optional<Dish> min = menu.stream().collect(minBy(Comparator.comparing(Dish::getCalories))); max.ifPresent(System.out::println); min.ifPresent(System.out::println);
也可以寫為:
Optional<Dish> max1 = menu.stream().max(Comparator.comparing(Dish::getCalories));
Optional<Dish> min1 = menu.stream().min(Comparator.comparing(Dish::getCalories));
2.匯總
Collectors.summingInt, 他接受一個把對象映射為求和的int函數,summingDouble ,summingLong用法一樣:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
Collectors.averagingInt,平均值,還有averagingDouble, averaginLong 用法 一樣:
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
summarizing 可以返回以上所有方法的數字:
IntSummaryStatistics menuStatics = menu.stream().collect(summarizingInt(Dish::getCalories));
menuStatics.getCount();
menuStatics.getMax();
menuStatics.getMin();
menuStatics.getSum();
menuStatics.getAverage();
3.連接字符串
joining方法返回的收集器會把對流中的每一個對象應用toString()然后連接成一個字符串。內部使用StringBuilder。 重載:參數是 分隔符
String names = menu.stream().map(Dish::getName).collect(joining());
String names1 = menu.stream().map(Dish::getName).collect(joining(","));
4.廣義的歸約匯總
其實我們討論的所有收集器,都是一個reducing工廠方法定義的歸約過程的特殊情況而已。Colectors.reducing工廠方法是所有這些特殊情況的一般化。例如總熱量:
int totalCalories1 = menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));
最大值:
Optional<Dish> max2 = menu.stream().collect(reducing((Dish d1, Dish d2)->d1.getCalories() > d2.getCalories() ? d1:d2));
本章中的collect歸約操作和上一章中reduce又有區別呢?
int totalCalories3 = menu.stream().map(Dish::getCalories).reduce(0, ( d1, d2) -> d1 + d2);
使用collect方法更利於並行操作。
counting方法也是使用的reducing工廠方法實現的:
public static <T> Collector<T, ?, Long> counting() { return reducing(0L, e -> 1L, Long::sum); }
這里的 ?通配符代表累加器類型未知,累加器本身可以是任何類型。
還有另一種不使用收集器也能執行相同操作,聚合:
int totalCalories2 = menu.stream().mapToInt(Dish::getCalories).sum();
還有:
int totalCalories4 = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
在這里是安全的,因為知道menu是不為空的。reduce返回的是Optional可空對象,最好是要配合orElse或orElseGet來獲得他的值更為安全。
函數式編程通常提供了多種方法來執行同一個操作。收集器在某種程度上比Stream接口上直接提供的方法用起來更復雜,但好處在於它們能提供更高水平的抽象和概括,也更容易重用和自定義。 盡可能的為手頭的問題探索不同的解決方案,比如聚合,更傾向於mapToInt,因為他最簡單明了,而且避免了自動裝箱。
分組
上面已經介紹過按菜的類型進行分組:
Map<Type, List<Dish>> group = menu.stream().collect(groupingBy(Dish::getType));
{OTHER=[Dish{Name='地三鮮', vegetarian=true, Calories=530, type=OTHER}, Dish{Name='水煮菠菜', vegetarian=true, Calories=350, type=OTHER}, Dish{Name='拔絲地瓜', vegetarian=true, Calories=120, type=OTHER}, Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}], MEAT=[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, Dish{Name='小炒牛肉', vegetarian=false, Calories=700, type=MEAT}, Dish{Name='宮保雞丁', vegetarian=false, Calories=400, type=MEAT}], FISH=[Dish{Name='水煮魚', vegetarian=false, Calories=330, type=FISH}, Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}]}
我們給groupingBy方法傳遞了一個Function(引用方法方式),它提取了流中每一道Dish的Dish.type。我們把這個Function叫做分類函數,因為它把流中的元素分成不同的組。分組的操作結果是一個Map,把分組函數返回的值作為映射的鍵,把流中所有具有這個分類值的列表作為對應的映射值。
分類函數還可以使用lmabda表達式來區分 高熱量 400-700的,低熱量 0-400的。
Map<String, List<Dish>> group1 = menu.stream().collect(groupingBy(c -> { if (c.getCalories() <= 400) { return "low"; } else if (c.getCalories() > 400) { return "higt"; } else { return "other"; } }));
{low=[Dish{Name='宮保雞丁', vegetarian=false, Calories=400, type=MEAT}, Dish{Name='水煮菠菜', vegetarian=true, Calories=350, type=OTHER}, Dish{Name='拔絲地瓜', vegetarian=true, Calories=120, type=OTHER}, Dish{Name='水煮魚', vegetarian=false, Calories=330, type=FISH}], higt=[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, Dish{Name='小炒牛肉', vegetarian=false, Calories=700, type=MEAT}, Dish{Name='地三鮮', vegetarian=true, Calories=530, type=OTHER}, Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}, Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}]}
並且,還可以同時根據熱量和type進行組合分組。
1.多級分組
想要實現多級分組,我們可以使用Collectors.groupingBy方法的雙參版本,它除了普通的分類函數之外,還可以接受collector類型的第二個參數。那么進行二級分組的話,我們可以把一個內層groupingBy傳遞給外層的groupingBy。
Map<Type, Map<String, List<Dish>>> group2 = menu.stream().collect( groupingBy(Dish::getType, groupingBy(c -> { if (c.getCalories() <= 400) { return "low"; } else if (c.getCalories() > 400) { return "higt"; } else { return "other"; } }) ) );
{OTHER={ low=[Dish{Name='水煮菠菜', vegetarian=true, Calories=350, type=OTHER}, Dish{Name='拔絲地瓜', vegetarian=true, Calories=120, type=OTHER}], higt=[Dish{Name='地三鮮', vegetarian=true, Calories=530, type=OTHER}, Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}]}, MEAT={ low=[Dish{Name='宮保雞丁', vegetarian=false, Calories=400, type=MEAT}], higt=[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, Dish{Name='小炒牛肉', vegetarian=false, Calories=700, type=MEAT}]}, FISH={ low=[Dish{Name='水煮魚', vegetarian=false, Calories=330, type=FISH}], higt=[Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}]}}
首先最外層是type,list值中又分 高地熱量分組 ,這種多級分組可以擴展至任意層級。
2.按子組收集數據
上面已經說過,可以把第二個groupingBy傳遞給第一個groupingBy,第二個收集器可以是任何類型,不一定是groupingBy,比如聚合coungting,來數一數每個類型下有多少菜。
Map<Type, Long> group3 = menu.stream().collect(groupingBy(Dish::getType, counting()));
{OTHER=4, MEAT=3, FISH=2}
其實groupingBy(f)實際上是groupingBy(f,toList())的簡單寫法。再舉一個例子,查看每個類型中最高熱量的菜:
Map<Type, Optional<Dish>> group4 = menu.stream().collect(groupingBy(Dish::getType, maxBy(Comparator.comparing(Dish::getCalories))));
{OTHER=Optional[Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}],
MEAT=Optional[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}],
FISH=Optional[Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}]}
這個map中的值是Optional,因為這是maxBy方法生成的收集器類型,但實際上,如果menu中沒有某一類型的Dish,這個類型就不會對應一個Optional.empty()值,而且根本不會出現在Map鍵值對中。
1.把收集器的結果轉換為另一種類型
因為分組操作的Map結果中的每個值上包裝的Optional沒什么用,所以想把它給去掉,也就是把收集器結果轉換為另一種類型,可以使用Collectors.collectingAndThen方法返回收集器。
Map<Type, Dish> group5 = menu.stream().collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(Comparator.comparing(Dish::getCalories)), Optional::get)));
{OTHER=Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}, MEAT=Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, FISH=Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}}
collectingAndThen方法參數1:要轉換的收集器,參數2:轉換函數。返回另一個收集器。 相當於舊收集器的包裝,collect操作的最后一步就是將返回值用轉換函數做一個映射。上面的例子,被包起來的收集器就是用maxBy建立的這個,而轉換函數Optional::get則把返回的Optional中的值提取出來。這個操作是安全的,因為reducing收集器永遠不會返回Optional.empty().
2.與groupingBy聯合使用的其他收集器的例子
每組菜熱量求和:
Map<Type, Integer> group6 = menu.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
{OTHER=1550, MEAT=1900, FISH=780}
groupingBy還常常和mapping方法組合,這個方法接受兩個參數:一個函數流中的元素做變換,另一個則將變換的結果對象收集起來。其目的是累加之前對每個輸入 元素應用一個映射函數,這樣就可以接受特定類型元素的收集器適應不同類型的對象,舉個例子,每個類型的菜,都有哪些超高熱量的菜:
Map<Type, Set<String>> group7 = menu.stream().collect(groupingBy(Dish::getType, mapping(c -> { if (c.getCalories() > 700) { return "super higt"; } else { return "super low"; } }, toSet()) ));
{OTHER=[super low], MEAT=[super higt, super low], FISH=[super low]}
傳遞給映射方法的轉換函數將Dish映射成了super higt 或super low字符串,傳遞給了一個toSet收集器,它和toList類似,不過是把流中的元素累計到了一個Set集合中。
還可以指定具體由哪個set類型,比如HashSet,可以使用toCollection:
Map<Type, HashSet<String>> group8 = menu.stream().collect(groupingBy(Dish::getType, mapping(c -> { if (c.getCalories() > 700) { return "super higt"; } else { return "super low"; } }, toCollection(HashSet::new)) ));
分區
分區是分組的特殊情況:由一個謂詞作為分類函數,它稱為分區函數。分區函數返回一個布爾值,這意味着得到的分組Map的鍵Boolean,於是他最多可以分為兩組true或false。例如:素食和葷菜分開:
Map<Boolean, List<Dish>> group9 = menu.stream().collect(groupingBy(Dish::isVegetarian));
{false=[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, Dish{Name='小炒牛肉', vegetarian=false, Calories=700, type=MEAT}, Dish{Name='宮保雞丁', vegetarian=false, Calories=400, type=MEAT}, Dish{Name='水煮魚', vegetarian=false, Calories=330, type=FISH}, Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}], true=[Dish{Name='地三鮮', vegetarian=true, Calories=530, type=OTHER}, Dish{Name='水煮菠菜', vegetarian=true, Calories=350, type=OTHER}, Dish{Name='拔絲地瓜', vegetarian=true, Calories=120, type=OTHER}, Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}]}
想獲取素食可以使用:
group9.get(true);
也可以使用Stream API:
List<Dish> stream = menu.stream().filter(Dish::isVegetarian).collect(toList());
1.分區的優勢
分區帶來的好處有兩點:1,因為保留了true和false,可以輕易獲取到false那一組。2,可以把分區作為groupingBy的第二個參數進行傳遞,產生一個二級分組:
Map<Type, Map<Boolean, List<Dish>>> group10 = menu.stream().collect(groupingBy(Dish::getType,
partitioningBy(Dish::isVegetarian)));
{OTHER={ true=[Dish{Name='地三鮮', vegetarian=true, Calories=530, type=OTHER}, Dish{Name='水煮菠菜', vegetarian=true, Calories=350, type=OTHER}, Dish{Name='拔絲地瓜', vegetarian=true, Calories=120, type=OTHER}, Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}]}, MEAT={ false=[Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, Dish{Name='小炒牛肉', vegetarian=false, Calories=700, type=MEAT}, Dish{Name='宮保雞丁', vegetarian=false, Calories=400, type=MEAT}]}, FISH={ false=[Dish{Name='水煮魚', vegetarian=false, Calories=330, type=FISH}, Dish{Name='於是乎', vegetarian=false, Calories=450, type=FISH}]}}
再比如,素食和非素食的最高熱量的菜:
Map<Boolean, Dish> group11 = menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(maxBy(Comparator.comparing(Dish::getCalories)), Optional::get)));
{false=Dish{Name='豬肉燉粉條', vegetarian=false, Calories=800, type=MEAT}, true=Dish{Name='火山下雪', vegetarian=true, Calories=550, type=OTHER}}
partitioningBy需要一個謂詞,也就是一個返回布爾表達的函數。
2.將數字按質數和非質數分區
一個大於1的自然數,除了1和它自身外,不能整除其他自然數的數叫做質數
public static boolean isPrime(int num) { return IntStream.range(2, num) .noneMatch(i -> num % i == 0); }
Map<Boolean, List<Integer>> group12 = IntStream.rangeClosed(2, 15).boxed() .collect( partitioningBy(n -> isPrime(n)) );
{false=[4, 6, 8, 9, 10, 12, 14, 15], true=[2, 3, 5, 7, 11, 13]}
Collectors類的靜態工廠方法
toList 返回類型:List<T> ,把流中的所有項目收集到一個List。
toSet 返回類型:Set<T>, 把流中的所有項目收集到一個Set。
toCollection 返回類型:Collection<T>, 把流中的所有項目收集到給定的供應源創建的集合。
counting 返回類型:Long , 計算流中元素的個數。
summingInt 返回類型:Integer , 對流中項目的一個整數屬性求和。
averagingInt 返回類型:Double , 計算流中項目Integer屬性的平均值。
summarizingInt 返回類型:IntSummaryStatistics , 收集關於流中項目Integer屬性的統計值,如最大、最小、總數、平均值。
joining 返回類型:String , 連接對流中每個項目調用toString方法鎖生成的字符串。
maxBy 返回類型:Optional<T> , 最大元素,如果流為空則Optional.empty();
minBy 返回類型:Optional<T> , 最小元素, 如果流為空則Optional.empty();
reducing 歸約操作產生的類型 , 從一個初始值開始,逐個累加,歸約為單個值。
collectingAndThen 轉換函數返回的類型 , 包裹另一個收集器,對其結果應用轉換函數。
groupingBy 返回類型:Map<K,List<T>> , 分組,將屬性值當做Map的鍵。
partitioningBy 返回類型 Map<Boolean,List<T>> , 分區,使用謂詞返回true或false的Map。
收集器接口
所有的收集器都是對Collector接口的實現,Collector接口包含了一系列方法,我們也可以自己提供實現,從而自由的創建自定義歸約操作。 首先我們來看一下Collector接口的定義:
public interface Collector<T, A, R> { Supplier<A> supplier(); BiConsumer<A, T> accumulator(); BinaryOperator<A> combiner(); Function<A, R> finisher(); Set<Characteristics> characteristics(); }
T:流中要收集的項目的泛型。
A:累加器的類型,累加器實在手機過程中用於累積部分結果的對象。
R:是收集操作得到的對象的類型。
1.理解Collector接口聲明的方法
前四個方法都會返回一個會被collect方法調用的函數,第五個方法characteristics則提供了一系列特征,也就是一個提示列表,告訴collect方法在執行歸約操作的時候可以應用哪些優化。
首先要創建一個類實現Collector接口
public class ToListCollector<T> implements Collector<T,List<T>,List<T>> { }
然后要建立新的結果容器:supplier方法
supplier方法必須返回一個結果為空的supplier,也就是一個無參函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用。
@Override public Supplier<List<T>> supplier() { // return ()->new ArrayList<T>(); //也可以使用方法引用如下: return ArrayList::new; }
將元素添加到結果容器:accumulator方法
accumulator方法會返回執行歸約操作的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約操作的累加器,還有第n個元素本身。該函數返回void,因為累加器是原始數據更新,即函數的執行改變了它的內部狀態以體現遍歷的元素的效果。
@Override public BiConsumer<List<T>, T> accumulator() { // return (list,item) -> list.add(item); //也可以使用方法引用 return List::add; }
對結果容器應用最終轉換:finisher方法
在遍歷完流后,finisher方法必須返回在累積過程的最后要調用的一個函數,以便將累加器對象轉換為整個集合做操的最終結果。通常,累加器對象恰好符合預期的最終結果,無需進行轉換,返回identity函數即可。
@Override public Function<List<T>, List<T>> finisher() { return Function.identity(); }
理論上這三個方法已經完成了歸約操作,實踐中實現細節可能還要更復雜一點,一方面是因為流的延遲性質,可能在collect操作之前還需要完成其他中間操作,另一方面則是理論上可能要進行並行歸約。
合並兩個結果容器:combiner方法
四個方法中的最后一個-combiner方法會返回一個供歸約操作使用的函數,它定義了對流的各個子部分進行並行處理時,各個子部分歸約所得的累加器要如何合並。對於toList而言,這個方法的實現非常簡單,只需要把流的第二個部分收集到項目列表加到遍歷第一部分時得到的列表后面就行了:
@Override public BinaryOperator<List<T>> combiner() { return (list1,list2)->{ list1.addAll(list2); return list1; }; }
有了第四個方法,我們就可以並行歸約了。
最后一個:characteristics方法
characteristics會返回一個不可變的Characteristics集合,它定義了收集器的行為-尤其是關於流是否可以進行並行歸約,以及可以使用那些優化的提示。Characteristics是一個包含三個項目的枚舉:
UNORDERED:歸約結果不受流中項目的遍歷和累積順序影響。
CONCURRENT:accumulator函數可以從多個線程同時調用,且該收集器可以並行歸約流。如果收集器沒有標為UNORDERED,那它僅在用於無需數據源時才可以並行歸約。
IDENTITY_FINISH:這表明完成器方法返回的函數是一個恆等函數,可以跳過。此時,累加器對象將會直接用作歸約過程的最終結果。意味着累加器A不加檢查的轉換為結果R是安全的。
我們開發的ToListCollector是IDENTITY_FINISH的,因為用來累積流中的元素的List已經是最終的結果了,用不着進一步轉換。但它不是UNORDERED,因為用在有序流上的時候,我們還是希望順序可以保留到List中,它是CONCURRENT的,僅僅在背后的數據源無序時才會並行處理。
2.全部融合到一起
public class ToListCollector<T> implements Collector<T,List<T>,List<T>> { @Override public Supplier<List<T>> supplier() { // return ()->new ArrayList<T>(); //也可以使用方法引用如下: return ArrayList::new; } @Override public BiConsumer<List<T>, T> accumulator() { // return (list,item) -> list.add(item); //也可以使用方法引用 return List::add; } @Override public Function<List<T>, List<T>> finisher() { return Function.identity(); } @Override public BinaryOperator<List<T>> combiner() { return (list1,list2)->{ list1.addAll(list2); return list1; }; } @Override public Set<Characteristics> characteristics() { return Collections.unmodifiableSet( EnumSet.of( Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT ) ); } }
這個實現與Collectors.toList方法並不完全相同,區別僅僅是一些小的優化。這個類鎖提供的收集器在返回空列表時使用了Collections.emptyList()。
menu.stream().collect(toList()); //工廠方法 menu.stream().collect(new ToListCollector<Dish>()); //需要實例化
對於IDENTITY_FINISH的收集操作,還可以不寫類實現Collector接口來自定義,Stream有一個重載的collect方法,可以接受另外三個函數-supplier、accmulator和combiner。
menu.stream().collect( ArrayList::new, //供應源 List::add, //累加器 List::addAll); //組合器
這種方式,寫法更為簡潔,這種方式不能傳遞任何Characteristics,所以他永遠都是一個IDENTITY_FINISH和CONCURRENT,但非UNORDERED。
小結:
1.collect是一個終端操作,它接受的參數是將流中元素累積到匯總結果的各種方式(收集器)。
2.預定義收集器包括將流元素歸約和匯總到一個值,例如最小值、最大值、平均值。
3.預定義收集器可以用groupingBy對流中的元素進行分組,或用partitioningBy進行分區。
4.收集器可以高效的組合使用,進行多級分組、分區和歸約。
5.可以通過實現Collector接口來自定義收集器。