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接口來定義的。
預定義的收集器
下面簡單演示基本的內置收集器。模擬數據源如下:
1
2
3
4
5
6
7
8
9
10
11
|
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) ); |
最大值,最小值,平均值
1
2
3
4
5
6
7
8
9
10
|
// 為啥返回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的元素拼起來?
1
2
3
4
|
//直接連接 string join1 = dishes.stream().map(dish::getname).collect(collectors.joining()); //逗號 string join2 = dishes.stream().map(dish::getname).collect(collectors.joining( ", " )); |
tolist
1
|
list<string> names = dishes.stream().map(dish::getname).collect(tolist()); |
將原來的stream映射為一個單元素流,然后收集為list。
toset
1
|
set<type> types = dishes.stream().map(dish::gettype).collect(collectors.toset()); |
將type收集為一個set,可以去重復。
tomap
1
|
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創建收集器。比如,求和
1
2
3
|
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
1
|
optional<integer> totalcalories3 = dishes.stream().map(dish::getcalories).reduce(integer::sum); |
雖然都可以,但考量效率的話,還是要選擇下面這種
1
|
int sum = dishes.stream().maptoint(dish::getcalories).sum(); |
根據情況選擇最佳方案
上面的demo說明,函數式編程通常提供了多種方法來執行同一個操作,使用收集器collect比直接使用stream的api用起來更加復雜,好處是collect能提供更高水平的抽象和概括,也更容易重用和自定義。
我們的建議是,盡可能為手頭的問題探索不同的解決方案,始終選擇最專業的一個,無論從可讀性還是性能來看,這一般都是最好的決定。
reducing除了接收一個初始值,還可以把第一項當作初始值
1
2
|
optional<dish> mostcaloriedish = dishes.stream() .collect(reducing((d1, d2) -> d1.getcalories() > d2.getcalories() ? d1 : d2)); |
reducing
關于reducing的用法比較復雜,目標在于把兩個值合并成一個值。
1
2
3
4
|
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里的第一個參數當做初始值。
1
2
|
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效果:
1
2
3
4
5
6
7
8
9
10
11
12
|
//手動實現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; } ); |
關于上述做法解釋一下。
1
2
3
|
<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。
1
|
map<type, list<dish>> dishesbytype = dishes.stream().collect(groupingby(dish::gettype)); |
這里
1
2
|
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,那么可以自定義分類器為:
1
2
3
4
5
6
7
8
9
|
private caloriclevel getcaloriclevel(dish d) { if (d.getcalories() <= 400 ) { return caloriclevel.diet; } else if (d.getcalories() <= 700 ) { return caloriclevel.normal; } else { return caloriclevel.fat; } } |
再傳入參數即可
1
2
|
map<caloriclevel, list<dish>> dishesbylevel = dishes.stream() .collect(groupingby( this ::getcaloriclevel)); |
多級分組
groupingby還重載了其他幾個方法,比如
1
2
3
|
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的源碼為:
1
2
3
4
|
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。
1
2
3
4
5
6
7
8
9
10
11
|
//多級分組 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); }); }); |
驗證結果為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
---------------------------------- 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
1
2
3
4
|
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
1
2
|
map<type, optional<dish>> mostcaloricbytype = dishes.stream() .collect(groupingby(dish::gettype, maxby(comparator.comparingint(dish::getcalories)))); |
這里的optional沒有意義,因為肯定不是null。那么只好取出來了。使用collectingandthen
1
2
3
|
map<type, dish> mostcaloricbytype = dishes.stream() .collect(groupingby(dish::gettype, collectingandthen(maxby(comparator.comparingint(dish::getcalories)), optional::get))); |
到這里似乎結果出來了,但idea不同意,編譯黃色報警,按提示修改后變為:
1
2
3
|
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則直接拋出異常:
1
2
|
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
1
2
|
map<type, integer> totalcaloriesbytype = dishes.stream() .collect(groupingby(dish::gettype, summingint(dish::getcalories))); |
然而常常和groupingby聯合使用的另一個收集器是mapping方法生成的。這個方法接收兩個參數:一個函數對流中的元素做變換,另一個則將變換的結果對象收集起來。其目的是在累加之前對每個輸入元素應用一個映射函數,這樣就可以讓接收特定類型元素的收集器適應不同類型的對象。我么來看一個使用這個收集器的實際例子。比如你想得到,對于每種類型的dish,菜單中都有哪些caloriclevel。我們可以把groupingby和mapping收集器結合起來,如下所示:
1
2
|
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. 例如,如果你是素食者,你可能想要把菜單按照素食和非素食分開:
1
|
map< boolean , list<dish>> partitionedmenu = dishes.stream().collect(partitioningby(dish::isvegetarian)); |
當然,使用filter可以達到同樣的效果:
1
|
list<dish> vegetariandishes = dishes.stream().filter(dish::isvegetarian).collect(collectors.tolist()); |
分區相對來說,優勢就是保存了兩個副本,當你想要對一個list分類時挺有用的。同時,和groupingby一樣,partitioningby一樣有重載方法,可以指定分組value的類型。
1
2
3
4
5
6
7
|
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收集器的最后一個例子,我們把菜單數據模型放在一邊,來看一個更加復雜也更為有趣的例子:將數組分為質數和非質數。
首先,定義個質數分區函數:
1
2
3
4
|
private boolean isprime( int candidate) { int candidateroot = ( int ) math.sqrt(( double ) candidate); return intstream.rangeclosed( 2 , candidateroot).nonematch(i -> candidate % i == 0 ); } |
然后找出1到100的質數和非質數
1
2
|
map< boolean , list<integer>> partitionprimes = intstream.rangeclosed( 2 , 100 ).boxed() .collect(partitioningby( this ::isprime)); |
原文鏈接:https://www.cnblogs.com/woshimrf/p/java8-collect-stream.html