您好,登錄后才能下訂單哦!
小編給大家分享一下Java8中Collect收集Stream的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
Collection, Collections, collect, Collector, Collectos
Collection是Java集合的祖先接口。
Collections是java.util包下的一個工具類,內涵各種處理集合的靜態方法。
java.util.stream.Stream#collect(java.util.stream.Collector<? super T,A,R>)是Stream的一個函數,負責收集流。
java.util.stream.Collector 是一個收集函數的接口, 聲明了一個收集器的功能。
java.util.Comparators則是一個收集器的工具類,內置了一系列收集器實現。
收集器的作用
你可以把Java8的流看做花哨又懶惰的數據集迭代器。他們支持兩種類型的操作:中間操作(e.g. filter, map)和終端操作(如count, findFirst, forEach, reduce). 中間操作可以連接起來,將一個流轉換為另一個流。這些操作不會消耗流,其目的是建立一個流水線。與此相反,終端操作會消耗類,產生一個最終結果。collect就是一個歸約操作,就像reduce一樣可以接受各種做法作為參數,將流中的元素累積成一個匯總結果。具體的做法是通過定義新的Collector接口來定義的。
預定義的收集器
下面簡單演示基本的內置收集器。模擬數據源如下:
final ArrayList<Dish> dishes = Lists.newArrayList( new Dish("pork", false, 800, Type.MEAT), new Dish("beef", false, 700, Type.MEAT), new Dish("chicken", false, 400, Type.MEAT), new Dish("french fries", true, 530, Type.OTHER), new Dish("rice", true, 350, Type.OTHER), new Dish("season fruit", true, 120, Type.OTHER), new Dish("pizza", true, 550, Type.OTHER), new Dish("prawns", false, 300, Type.FISH), new Dish("salmon", false, 450, Type.FISH) );
最大值,最小值,平均值
// 為啥返回Optional? 如果stream為null怎么辦, 這時候Optinal就很有意義了 Optional<Dish> mostCalorieDish = dishes.stream().max(Comparator.comparingInt(Dish::getCalories)); Optional<Dish> minCalorieDish = dishes.stream().min(Comparator.comparingInt(Dish::getCalories)); Double avgCalories = dishes.stream().collect(Collectors.averagingInt(Dish::getCalories)); IntSummaryStatistics summaryStatistics = dishes.stream().collect(Collectors.summarizingInt(Dish::getCalories)); double average = summaryStatistics.getAverage(); long count = summaryStatistics.getCount(); int max = summaryStatistics.getMax(); int min = summaryStatistics.getMin(); long sum = summaryStatistics.getSum();
這幾個簡單的統計指標都有Collectors內置的收集器函數,尤其是針對數字類型拆箱函數,將會比直接操作包裝類型開銷小很多。
連接收集器
想要把Stream的元素拼起來?
//直接連接 String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining()); //逗號 String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));
toList
List<String> names = dishes.stream().map(Dish::getName).collect(toList());
將原來的Stream映射為一個單元素流,然后收集為List。
toSet
Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());
將Type收集為一個set,可以去重復。
toMap
Map<Type, Dish> byType = dishes.stream().collect(toMap(Dish::getType, d -> d));
有時候可能需要將一個數組轉為map,做緩存,方便多次計算獲取。toMap提供的方法k和v的生成函數。(注意,上述demo是一個坑,不可以這樣用!!!, 請使用toMap(Function, Function, BinaryOperator))
上面幾個幾乎是最常用的收集器了,也基本夠用了。但作為初學者來說,理解需要時間。想要真正明白為什么這樣可以做到收集,就必須查看內部實現,可以看到,這幾個收集器都是基于java.util.stream.Collectors.CollectorImpl,也就是開頭提到過了Collector的一個實現類。后面自定義收集器會學習具體用法。
自定義歸約reducing
前面幾個都是reducing工廠方法定義的歸約過程的特殊情況,其實可以用Collectors.reducing創建收集器。比如,求和
Integer totalCalories = dishes.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j)); //使用內置函數代替箭頭函數 Integer totalCalories2 = dishes.stream().collect(reducing(0, Dish::getCalories, Integer::sum));
當然也可以直接使用reduce
Optional<Integer> totalCalories3 = dishes.stream().map(Dish::getCalories).reduce(Integer::sum);
雖然都可以,但考量效率的話,還是要選擇下面這種
int sum = dishes.stream().mapToInt(Dish::getCalories).sum();
根據情況選擇最佳方案
上面的demo說明,函數式編程通常提供了多種方法來執行同一個操作,使用收集器collect比直接使用stream的api用起來更加復雜,好處是collect能提供更高水平的抽象和概括,也更容易重用和自定義。
我們的建議是,盡可能為手頭的問題探索不同的解決方案,始終選擇最專業的一個,無論從可讀性還是性能來看,這一般都是最好的決定。
reducing除了接收一個初始值,還可以把第一項當作初始值
Optional<Dish> mostCalorieDish = dishes.stream() .collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
reducing
關于reducing的用法比較復雜,目標在于把兩個值合并成一個值。
public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op)
首先看到3個泛型,
U是返回值的類型,比如上述demo中計算熱量的,U就是Integer。
關于T,T是Stream里的元素類型。由Function的函數可以知道,mapper的作用就是接收一個參數T,然后返回一個結果U。對應demo中Dish。
?在返回值Collector的泛型列表的中間,這個表示容器類型,一個收集器當然需要一個容器來存放數據。這里的?則表示容器類型不確定。事實上,在這里的容器就是U[]。
關于參數:
identity是返回值類型的初始值,可以理解為累加器的起點。
mapper則是map的作用,意義在于將Stream流轉換成你想要的類型流。
op則是核心函數,作用是如何處理兩個變量。其中,第一個變量是累積值,可以理解為sum,第二個變量則是下一個要計算的元素。從而實現了累加。
reducing還有一個重載的方法,可以省略第一個參數,意義在于把Stream里的第一個參數當做初始值。
public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
先看返回值的區別,T表示輸入值和返回值類型,即輸入值類型和輸出值類型相同。還有不同的就是Optional了。這是因為沒有初始值,而第一個參數有可能是null,當Stream的元素是null的時候,返回Optional就很意義了。
再看參數列表,只剩下BinaryOperator。BinaryOperator是一個三元組函數接口,目標是將兩個同類型參數做計算后返回同類型的值。可以按照1>2? 1:2來理解,即求兩個數的最大值。求最大值是比較好理解的一種說法,你可以自定義lambda表達式來選擇返回值。那么,在這里,就是接收兩個Stream的元素類型T,返回T類型的返回值。用sum累加來理解也可以。
上述的demo中發現reduce和collect的作用幾乎一樣,都是返回一個最終的結果,比如,我們可以使用reduce實現toList效果:
//手動實現toListCollector --- 濫用reduce, 不可變的規約---不可以并行 List<Integer> calories = dishes.stream().map(Dish::getCalories) .reduce(new ArrayList<Integer>(), (List<Integer> l, Integer e) -> { l.add(e); return l; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; } );
關于上述做法解釋一下。
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
U是返回值類型,這里就是List
BiFunction<U, ? super T, U> accumulator是是累加器,目標在于累加值和單個元素的計算規則。這里就是List和元素做運算,最終返回List。即,添加一個元素到list。
BinaryOperator<U> combiner是組合器,目標在于把兩個返回值類型的變量合并成一個。這里就是兩個list合并。
這個解決方案有兩個問題:一個是語義問題,一個是實際問題。語義問題在于,reduce方法旨在把兩個值結合起來生成一個新值,它是一個不可變歸約。相反,collect方法的設計就是要改變容器,從而累積要輸出的結果。這意味著,上面的代碼片段是在濫用reduce方法,因為它在原地改變了作為累加器的List。錯誤的語義來使用reduce方法還會造成一個實際問題:這個歸約不能并行工作,因為由多個線程并發修改同一個數據結構可能會破壞List本身。在這種情況下,如果你想要線程安全,就需要每次分配一個新的List,而對象分配又會影響性能。這就是collect適合表達可變容器上的歸約的原因,更關鍵的是它適合并行操作。
總結:reduce適合不可變容器歸約,collect適合可變容器歸約。collect適合并行。
分組
數據庫中經常遇到分組求和的需求,提供了group by原語。在Java里, 如果按照指令式風格(手動寫循環)的方式,將會非常繁瑣,容易出錯。而Java8則提供了函數式解法。
比如,將dish按照type分組。和前面的toMap類似,但分組的value卻不是一個dish,而是一個List。
Map<Type, List<Dish>> dishesByType = dishes.stream().collect(groupingBy(Dish::getType));
這里
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
參數分類器為Function,旨在接收一個參數,轉換為另一個類型。上面的demo就是把stream的元素dish轉成類型Type,然后根據Type將stream分組。其內部是通過HashMap來實現分組的。groupingBy(classifier, HashMap::new, downstream);
除了按照stream元素自身的屬性函數去分組,還可以自定義分組依據,比如根據熱量范圍分組。
既然已經知道groupingBy的參數為Function, 并且Function的參數類型為Dish,那么可以自定義分類器為:
private CaloricLevel getCaloricLevel(Dish d) { if (d.getCalories() <= 400) { return CaloricLevel.DIET; } else if (d.getCalories() <= 700) { return CaloricLevel.NORMAL; } else { return CaloricLevel.FAT; } }
再傳入參數即可
Map<CaloricLevel, List<Dish>> dishesByLevel = dishes.stream() .collect(groupingBy(this::getCaloricLevel));
多級分組
groupingBy還重載了其他幾個方法,比如
public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
泛型多的恐怖。簡單的認識一下。classifier還是分類器,就是接收stream的元素類型,返回一個你想要分組的依據,也就是提供分組依據的基數的。所以T表示stream當前的元素類型,K表示分組依據的元素類型。第二個參數downstream,下游是一個收集器Collector. 這個收集器元素類型是T的子類,容器類型container為A,reduction返回值類型為D。也就是說分組的K通過分類器提供,分組的value則通過第二個參數的收集器reduce出來。正好,上個demo的源碼為:
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) { return groupingBy(classifier, toList()); }
將toList當作reduce收集器,最終收集的結果是一個List<Dish>, 所以分組結束的value類型是List<Dish>。那么,可以類推value類型取決于reduce收集器,而reduce收集器則有千千萬。比如,我想對value再次分組,分組也是一種reduce。
//多級分組 Map<Type, Map<CaloricLevel, List<Dish>>> byTypeAndCalory = dishes.stream().collect( groupingBy(Dish::getType, groupingBy(this::getCaloricLevel))); byTypeAndCalory.forEach((type, byCalory) -> { System.out.println("----------------------------------"); System.out.println(type); byCalory.forEach((level, dishList) -> { System.out.println("\t" + level); System.out.println("\t\t" + dishList); }); });
驗證結果為:
---------------------------------- FISH DIET [Dish(name=prawns, vegetarian=false, calories=300, type=FISH)] NORMAL [Dish(name=salmon, vegetarian=false, calories=450, type=FISH)] ---------------------------------- MEAT FAT [Dish(name=pork, vegetarian=false, calories=800, type=MEAT)] DIET [Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)] NORMAL [Dish(name=beef, vegetarian=false, calories=700, type=MEAT)] ---------------------------------- OTHER DIET [Dish(name=rice, vegetarian=true, calories=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calories=120, type=OTHER)] NORMAL [Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]
總結:groupingBy的核心參數為K生成器,V生成器。V生成器可以是任意類型的收集器Collector。
比如,V生成器可以是計算數目的, 從而實現了sql語句中的select count(*) from table A group by Type
Map<Type, Long> typesCount = dishes.stream().collect(groupingBy(Dish::getType, counting())); System.out.println(typesCount); ----------- {FISH=2, MEAT=3, OTHER=4}
sql查找分組最高分select MAX(id) from table A group by Type
Map<Type, Optional<Dish>> mostCaloricByType = dishes.stream() .collect(groupingBy(Dish::getType, maxBy(Comparator.comparingInt(Dish::getCalories))));
這里的Optional沒有意義,因為肯定不是null。那么只好取出來了。使用collectingAndThen
Map<Type, Dish> mostCaloricByType = dishes.stream() .collect(groupingBy(Dish::getType, collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
到這里似乎結果出來了,但IDEA不同意,編譯黃色報警,按提示修改后變為:
Map<Type, Dish> mostCaloricByType = dishes.stream() .collect(toMap(Dish::getType, Function.identity(), BinaryOperator.maxBy(comparingInt(Dish::getCalories))));
是的,groupingBy就變成toMap了,key還是Type,value還是Dish,但多了一個參數!!這里回應開頭的坑,開頭的toMap演示是為了容易理解,真那么用則會被搞死。我們知道把一個List重組為Map必然會面臨k相同的問題。當K相同時,v是覆蓋還是不管呢?前面的demo的做法是當k存在時,再次插入k則直接拋出異常:
java.lang.IllegalStateException: Duplicate key Dish(name=pork, vegetarian=false, calories=800, type=MEAT) at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
正確的做法是提供處理沖突的函數,在本demo中,處理沖突的原則就是找出最大的,正好符合我們分組求最大的要求。(真的不想搞Java8函數式學習了,感覺到處都是性能問題的坑)
繼續數據庫sql映射,分組求和select sum(score) from table a group by Type
Map<Type, Integer> totalCaloriesByType = dishes.stream() .collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
然而常常和groupingBy聯合使用的另一個收集器是mapping方法生成的。這個方法接收兩個參數:一個函數對流中的元素做變換,另一個則將變換的結果對象收集起來。其目的是在累加之前對每個輸入元素應用一個映射函數,這樣就可以讓接收特定類型元素的收集器適應不同類型的對象。我么來看一個使用這個收集器的實際例子。比如你想得到,對于每種類型的Dish,菜單中都有哪些CaloricLevel。我們可以把groupingBy和mapping收集器結合起來,如下所示:
Map<Type, Set<CaloricLevel>> caloricLevelsByType = dishes.stream() .collect(groupingBy(Dish::getType, mapping(this::getCaloricLevel, toSet())));
這里的toSet默認采用的HashSet,也可以手動指定具體實現toCollection(HashSet::new)
分區
分區是分組的特殊情況:由一個謂詞(返回一個布爾值的函數)作為分類函數,它稱為分區函數。分區函數返回一個布爾值,這意味著得到的分組Map的鍵類型是Boolean,于是它最多可以分為兩組:true or false. 例如,如果你是素食者,你可能想要把菜單按照素食和非素食分開:
Map<Boolean, List<Dish>> partitionedMenu = dishes.stream().collect(partitioningBy(Dish::isVegetarian));
當然,使用filter可以達到同樣的效果:
List<Dish> vegetarianDishes = dishes.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
分區相對來說,優勢就是保存了兩個副本,當你想要對一個list分類時挺有用的。同時,和groupingBy一樣,partitioningBy一樣有重載方法,可以指定分組value的類型。
Map<Boolean, Map<Type, List<Dish>>> vegetarianDishesByType = dishes.stream() .collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType))); Map<Boolean, Integer> vegetarianDishesTotalCalories = dishes.stream() .collect(partitioningBy(Dish::isVegetarian, summingInt(Dish::getCalories))); Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = dishes.stream() .collect(partitioningBy(Dish::isVegetarian, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));
作為使用partitioningBy收集器的最后一個例子,我們把菜單數據模型放在一邊,來看一個更加復雜也更為有趣的例子:將數組分為質數和非質數。
首先,定義個質數分區函數:
private boolean isPrime(int candidate) { int candidateRoot = (int) Math.sqrt((double) candidate); return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0); }
然后找出1到100的質數和非質數
Map<Boolean, List<Integer>> partitionPrimes = IntStream.rangeClosed(2, 100).boxed() .collect(partitioningBy(this::isPrime));
以上是“Java8中Collect收集Stream的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。