JAVA8 stream中三個引數的reduce方法對List進行分組統計操作
背景
平時在編寫前端程式碼時,習慣使用lodash來編寫‘野生'的JavaScript;
lodash提供來一套完整的API對JS物件(Array,Object,Collection等)進行操作,這其中就包括_.groupBy 和 _.reduce,即分組和'聚合'(reduce不知道該怎麼翻譯合適)。
使用這些‘野生'的API能夠極大的提高我本人編寫JS程式碼的效率。而JAVA8開始支援stream和lambda表示式,這些和lodash的API有很多類似的功能。因此我在熟悉lodash的前提下嘗試使用JAVA8的新特性減少冗餘程式碼的編寫。
需求
在開發後端某功能介面的過程中,需要對一個從資料庫中取出的資料List<T>進行按照ID進行聚合統計
JAVA8 reduce API
API個人理解
<U> U reduce(U u,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner) #第一個引數返回例項u,傳遞你要返回的U型別物件的初始化例項u #第二個引數累加器accumulator,可以使用二元ℷ表示式(即二元lambda表示式),宣告你在u上累加你的資料來源t的邏輯 #例如(u,t)->u.sum(t),此時lambda表示式的行參列表是返回例項u和遍歷的集合元素t,函式體是在u上累加t #第三個引數組合器combiner,同樣是二元ℷ表示式,(u,t)->u #lambda表示式行參列表同樣是(u,t),函式體返回的型別則要和第一個引數的型別保持一致
虛擬碼
#1.宣告一個返回結果U #2.對List<T>進行遍歷,在U和每個T例項上應用一個累加器進行累加操作 #3.返回最終結果U U result = identity; for (T element : this stream) result = accumulator.apply(result,element) return result;
資料準備
var source = [ {"name": "A","type": "san","typeValue": 1.0,"count": 2},{"name": "A","type": "nas","typeValue": 13.0,"count": 1},{"name": "B","typeValue": 112.0,"count": 3},{"name": "C","typeValue": 43.0,"count": 5},"typeValue": 77.0,"count": 7} ]; var target = [ { "name": "A","count": 3,"totalTypeValue": 14.0,"bazList": [ { "type": "san","typeValue": 1.0 },{ "type": "nas" "typeValue": 13.0 } ] },{ "name": "B","count": 10,"totalTypeValue": 189.0,"typeValue": 112.0 },{ "type": "nas" "typeValue": 77.0 } ] },{ "name": "C","count": 5,"totalTypeValue": 43.0,"typeValue": 43.0 } ] } ];
Code
講了那麼多廢話,這個才是最直接的
程式碼執行大意
對 List<Foo> 按照name分組統計得到 List<Bar>
ReduceTest.java
import com.google.common.collect.Lists; import Bar; import Foo; import java.util.List; import java.util.stream.Collectors; public class ReduceTest { public static void main(String[] args) throws Exception{ List<Foo> fooList = Lists.newArrayList( new Foo("A","san",1.0,2),new Foo("A","nas",13.0,1),new Foo("B",112.0,3),new Foo("C",43.0,5),77.0,7) ); List<Bar> barList = Lists.newArrayList(); fooList .stream() .collect(Collectors.groupingBy(Foo::getName,Collectors.toList())) .forEach((name,fooListByName)->{ Bar bar = new Bar(); bar = fooListByName .stream() .reduce(bar,(u,t)->u); System.out.println(bar.toString()); barList.add(bar); }); } /* 輸出結果 name:A count:3 totalTypeValue:14.0 bazList: type:san typeValue:1.0 type:nas typeValue:13.0 name:B count:10 totalTypeValue:189.0 bazList: type:san typeValue:112.0 type:nas typeValue:77.0 name:C count:5 totalTypeValue:43.0 bazList: type:san typeValue:43.0 */ }
Foo.java
public class Foo{ private String name; private String type; private Double typeValue; private Integer count; public Foo(String name,String type,Double typeValue,Integer count) { this.name = name; this.type = type; this.typeValue = typeValue; this.count = count; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getType() { return type; } public void setType(String type) { this.type = type; } public Double getTypeValue() { return typeValue; } public void setTypeValue(Double typeValue) { this.typeValue = typeValue; } public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } }
Bar.java
import com.google.common.collect.Lists; import java.util.List; public class Bar{ private String name; private Integer count; private Double totalTypeValue; private List<Baz> bazList; public Bar() { this.name = null; this.count = 0; this.totalTypeValue = 0.0; this.bazList = Lists.newArrayList(); } public Bar sum(Foo foo){ if(name == null){ this.name = foo.getName(); } this.count += foo.getCount(); this.totalTypeValue += foo.getTypeValue(); this.bazList.add(new Baz(foo.getType(),foo.getTypeValue())); return this; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("name:").append(this.name).append(System.lineSeparator()); sb.append("count:").append(this.count).append(System.lineSeparator()); sb.append("totalTypeValue:").append(this.totalTypeValue).append(System.lineSeparator()); sb.append("bazList:").append(System.lineSeparator()); this.bazList.forEach(baz->{ sb.append("\t").append("type:").append(baz.getType()).append(System.lineSeparator()); sb.append("\t").append("typeValue:").append(baz.getTypeValue()).append(System.lineSeparator()); }); return sb.toString(); } }
Baz.java
public class Baz{ private String type; private Double typeValue; public Baz(String type,Double typeValue) { this.type = type; this.typeValue = typeValue; } public String getType() { return type; } public void setType(String type) { this.type = type; } public Double getTypeValue() { return typeValue; } public void setTypeValue(Double typeValue) { this.typeValue = typeValue; } }
PS
等下次有空補上不使用stream().reduce 實現同樣操作的比較繁瑣的程式碼,啦啦啦啦啦~~~
補充知識:Java8collect、reduce方法聚合操作詳解
Stream的基本概念
Stream和集合的區別:
1.Stream不會自己儲存元素。元素儲存在底層集合或者根據需要產生。
2.Stream操作符不會改變源物件。相反,它會返回一個持有結果的新的Stream。
3.Stream操作可能是延遲執行的,這意味著它們會等到需要結果的時候才執行。
Stream操作的基本過程,可以歸結為3個部分:
建立一個Stream。
在一個或者多個操作中,將指定的Stream轉換為另一個Stream的中間操作。
通過終止(terminal)方法來產生一個結果。該操作會強制它之前的延時操作立即執行,這之後該Stream就不能再被使用了。
中間操作都是filter()、distinct()、sorted()、map()、flatMap()等,其一般是對資料集的整理(過濾、排序、匹配、抽取等)。
終止方法往往是完成對資料集中資料的處理,如forEach(),還有allMatch()、anyMatch()、findAny()、 findFirst(),數值計算類的方法有sum、max、min、average等等。終止方法也可以是對集合的處理,如reduce()、 collect()等等。
reduce()方法的處理方式一般是每次都產生新的資料集,而collect()方法是在原資料集的基礎上進行更新,過程中不產生新的資料集。
List nums = Arrays.asList(1,3,null,8,7,13,10);
nums.stream().filter(num -> num != null).distinct().forEach(System.out::println);
上面程式碼實現為過濾null值並去重,遍歷結果,實現簡潔明瞭。使用傳統方法就相對繁瑣的多。另外其中 forEach即為終止操作方法,如果無該方法上面程式碼就沒有任何操作。
filter、map、forEach、findAny等方法的使用都比較簡單,這裡省略。
下面介紹強大的聚合操作,其主要分為兩種:
可變聚合:把輸入的元素們累積到一個可變的容器中,比如Collection或者StringBuilder;
其他聚合:除去可變聚合,剩下的,一般都不是通過反覆修改某個可變物件,而是通過把前一次的匯聚結果當成下一次的入參,反覆如此。比如reduce,count,allMatch;
聚合操作reduce
Stream.reduce,返回單個的結果值,並且reduce操作每處理一個元素總是建立一個新值。常用的方法有average,sum,min,max,count,使用reduce方法都可實現。
這裡主要介紹reduce方法:
T reduce(T identity,BinaryOperator accumulator)
identity:它允許使用者提供一個迴圈計算的初始值。accumulator:計算的累加器,其方法簽名為apply(T t,U u),在該reduce方法中第一個引數t為上次函式計算的返回值,第二個引數u為Stream中的元素,這個函式把這兩個值計算apply,得到的和會被賦值給下次執行這個方法的第一個引數。有點繞看程式碼:
int value = Stream.of(1,2,4).reduce(100,(sum,item) -> sum + item); Assert.assertSame(value,110); /* 或者使用方法引用 */ value = Stream.of(1,Integer::sum);
這個例子中100即為計算初始值,每次相加計算值都會傳遞到下一次計算的第一個引數。
reduce還有其它兩個過載方法:
Optional reduce(BinaryOperatoraccumulator):與上面定義基本一樣,無計算初始值,所以他返回的是一個Optional。
U reduce(U identity,BiFunction accumulator,BinaryOperator combiner):與前面兩個引數的reduce方法幾乎一致,你只要注意到BinaryOperator其實實現了BiFunction和BinaryOperator兩個介面。
收集結果collect
當你處理完流時,通常只是想檢視一下結果,而不是將他們聚合為一個值。先看collect的基礎方法,它接受三個引數:
R collect(Supplier supplier,BiConsumer accumulator,BiConsumer combiner)
supplier:一個能創造目標型別例項的方法。accumulator:一個將當元素新增到目標中的方法。combiner:一個將中間狀態的多個結果整合到一起的方法(併發的時候會用到)。接著看程式碼:
Stream stream = Stream.of(1,4).filter(p -> p > 2); List result = stream.collect(() -> new ArrayList<>(),(list,item) -> list.add(item),(one,two) -> one.addAll(two)); /* 或者使用方法引用 */ result = stream.collect(ArrayList::new,List::add,List::addAll);
這個例子即為過濾大於2的元素,將剩餘結果收集到一個新的list中。
第一個方法生成一個新的ArrayList;
第二個方法中第一個引數是前面生成的ArrayList物件,第二個引數是stream中包含的元素,方法體就是把stream中的元素加入ArrayList物件中。第二個方法被反覆呼叫直到原stream的元素被消費完畢;
第三個方法也是接受兩個引數,這兩個都是ArrayList型別的,方法體就是把第二個ArrayList全部加入到第一個中;
程式碼有點繁瑣,或者使用collect的另一個過載方法:
R collect(Collector collector)
注意到Collector其實是上面supplier、accumulator、combiner的聚合體。那麼上面程式碼就變成:
List list = Stream.of(1,4).filter(p -> p > 2).collect(Collectors.toList());
將結果收集到map中
先定義如下Person物件
class Person{ public String name; public int age; Person(String name,int age){ this.name = name; this.age = age; } @Override public String toString(){ return String.format("Person{name='%s',age=%d}",name,age); } }
假設你有一個Stream物件,希望將其中元素收集到一個map中,這樣就可以根據他的名稱來查詢對應年齡,例如:
Map result = people.collect(HashMap::new,(map,p)->map.put(p.name,p.age),Map::putAll); /*使用Collectors.toMap形式*/
Map result = people.collect(Collectors.toMap(p -> p.name,p -> p.age,(exsit,newv) -> newv));
其中Collectors.toMap方法的第三個引數為鍵值重複處理策略,如果不傳入第三個引數,當有相同的鍵時,會丟擲一個IlleageStateException。
或者你想將Person分解為Map儲存:
List<Map> personToMap = people.collect(ArrayList::new,p) -> { Mapmap = new HashMap<>(); map.put("name",p.name); map.put("age",p.age); list.add(map); },List::addAll);
分組和分片
對具有相同特性的值進行分組是一個很常見的任務,Collectors提供了一個groupingBy方法,方法簽名為:
Collector<T,?,Map> groupingBy(Function classifier,Collector downstream)
classifier:一個獲取Stream元素中主鍵方法。downstream:一個操作對應分組後的結果的方法。
假如要根據年齡來分組:
Map<Integer,List> peopleByAge = people.filter(p -> p.age > 12).collect(Collectors.groupingBy(p -> p.age,Collectors.toList()));
假如我想要根據年齡分組,年齡對應的鍵值List儲存的為Person的姓名,怎麼做呢:
Map<Integer,List> peopleByAge = people.collect(Collectors.groupingBy(p -> p.age,Collectors.mapping((Person p) -> p.name,Collectors.toList())));
mapping即為對各組進行投影操作,和Stream的map方法基本一致。
假如要根據姓名分組,獲取每個姓名下人的年齡總和(好像需求有些坑爹):
Map sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name,Collectors.reducing(0,(Person p) -> p.age,Integer::sum))); /* 或者使用summingInt方法 */ sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name,Collectors.summingInt((Person p) -> p.age)));
可以看到Java8的分組功能相當強大,當然你還可以完成更復雜的功能。另外Collectors中還存在一個類似groupingBy的方法:partitioningBy,它們的區別是partitioningBy為鍵值為Boolean型別的groupingBy,這種情況下它比groupingBy更有效率。
join和統計功能
話說Java8中新增了一個StringJoiner,Collectors的join功能和它基本一樣。用於將流中字串拼接並收集起來,使用很簡單:
String names = people.map(p->p.name).collect(Collectors.joining(","))
Collectors分別提供了求平均值averaging、總數couting、最小值minBy、最大值maxBy、求和suming等操作。但是假如你希望將流中結果聚合為一個總和、平均值、最大值、最小值,那麼Collectors.summarizing(Int/Long/Double)就是為你準備的,它可以一次行獲取前面的所有結果,其返回值為(Int/Long/Double)SummaryStatistics。
DoubleSummaryStatistics dss = people.collect(Collectors.summarizingDouble((Person p)->p.age)); double average=dss.getAverage(); double max=dss.getMax(); double min=dss.getMin(); double sum=dss.getSum(); double count=dss.getCount();
以上這篇JAVA8 stream中三個引數的reduce方法對List進行分組統計操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。