在前面已經使用過collect終端操做了,主要是用來把Stream中的全部元素結合成一個List,在本章中,你會發現collect是一個歸約操做,就像reduce同樣能夠接受各類作法做爲參數,將流中的元素累計成一個彙總結果。java
看這個例子:按照菜類進行分組編程
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));
收集器簡介app
在上一個例子中,你只須要給出指令「作什麼」,而不是編寫實現步驟「如何作」。之前toList()方法只是說按順序給每個元素生成一個列表。在這個例子中,groupingBy說的是生成一個Map,它的鍵是菜的種類,他們值是那些菜。ide
1.收集器用做高級歸約函數式編程
對流調用collect方法將對流中的元素觸發一個歸約操做,它遍歷流中的每個元素,並讓Collector進行處理。如toList靜態方法,他會把流中的每個元素收集到一個list中。函數
2.預約義收集器優化
Collectors類提供的工廠方法建立的收集器,他們主要提供了三大功能:將流元素歸約和彙總爲一個值、元素分組、元素分區。ui
歸約和彙總spa
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接口來自定義收集器。