說明
java8集合中Stream()相關函數都支持lambda表達式,reduce()就是其中之一,
reduce是一種聚合操作,聚合的含義就是將多個值經過特定計算之後得到單個值,
常見的 count 、sum 、avg 、max 、min 等函數就是一種聚合操作。
本文使用reduce函數做求和計算來說明它的用法:
reduce有三個重載方法
1.一個參數的reduce
Optional<T> reduce(BinaryOperator<T> accumulator);
參數: BinaryOperator<T> accumulator
, BinaryOperator 繼承於 BiFunction, 這裏實現 BiFunction.apply(param1,param2)
接口即可。支持lambda表達式,形如:(result,item)->{...}
。
返回值:返回Optional對象,由於結果存在空指針的情況(當集合爲空時)因此需要使用Optional。
如下代碼通過reduce 求整數集合中的元素之和:
import com.google.common.collect.Lists;
import java.util.List;
public class LambdaTest {
public static void main(String[] args) {
List<Integer> list=Lists.newArrayList(1,2,3,4,5);
//將數組進行累加求和
//由於返回的是 Optional ,因此需要get()取出值。
Integer total=list.stream().reduce((result,item)->result+item).get();
System.out.println(total);
}
}
//結果爲: 15
將累加的每一步打印,可以發現Lambda表達式中的兩個參數(result,item)的含義:
第一個參數 result :初始值爲集合中的第一個元素,後面爲每次的累加計算結果 ;
第二個參數 item :遍歷的集合中的每一個元素(從第二個元素開始,第一個被result使用了)。
List<Integer> list=Lists.newArrayList(1,2,3,4,5);
list.stream().reduce((result,item)->{
System.out.println("result="+result+", item="+item);
return result+item;
});
/* 結果如下:
result=1, item=2
result=3, item=3
result=6, item=4
result=10, item=5
*/
2.兩個參數的reduce
T reduce(T identity, BinaryOperator<T> accumulator);
參數1:T identity
爲一個初始值(默認值) ,當集合爲空時,就返回這個默認值,當集合不爲空時,該值也會參與計算;
參數2:BinaryOperator<T> accumulator
這個與一個參數的reduce相同。
返回值:並非 Optional,由於有默認值 identity ,因此計算結果不存在空指針的情況。
List<Integer> list=Lists.newArrayList(1,2,3,4,5);
Integer total=list.stream().reduce(0,(result,item)->result+item);
System.out.println(total);//結果爲:15
list=new ArrayList<>();
total=list.stream().reduce(0,(result,item)->result+item);
System.out.println(total);//數組爲空時,結果返回默認值0
3.三個參數的reduce
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
第一個參數和第二個參數的定義同上,第三個參數比較特殊,後面慢慢講。
可以看到該方法有兩個泛型 T 和 U :
(1)泛型T是集合中元素的類型,
(2)泛型U是計算之後返回結果的類型,U的類型由第一個參數 identity 決定。
也就是說,三個參數的reduce()可以返回與集合中的元素不同類型的值,方便我們對複雜對象做計算式和轉換。
而一個參數和兩個參數的reduce()只能返回與集合中的元素同類型的值。
現在我們在集合中存放 ScoreBean 對象,模擬學生分數統計:
static class ScoreBean {
private String name; //學生姓名
private int score; //分數,需要彙總該字段
public ScoreBean(String name, int score) {
this.name = name;
this.score = score;
}
//get 和 set 方法省略
}
我們對 ScoreBean 中 score 字段彙總:(後面示例代碼中的list定義省略,都用這個)
List<ScoreBean> list= Lists.newArrayList(
new ScoreBean("張三",1)
,new ScoreBean("李四",2)
,new ScoreBean("王五",3)
,new ScoreBean("小明",4)
,new ScoreBean("小紅",5));
Integer total=list.stream()
.reduce(
Integer.valueOf(0) /*初始值 identity*/
,(integer,scoreBean)->integer+scoreBean.getScore() /*累加計算 accumulator*/
,(integer1,integer2)->integer1+integer2 /*第三個參數 combiner*/
);
System.out.println(total);//結果:15
其實這個相當於:
Integer total=list.stream().mapToInt(ScoreBean::getScore).sum();
System.out.println(total);//結果也是:15
第三個參數 BinaryOperator<U> combiner
是個什麼鬼?
這個參數的lambda表達式我是這麼寫的:(integer1,integer2)->integer1+integer2)
現在我將其打印出來:
Integer total=list.stream()
.reduce(
Integer.valueOf(0)
,(integer,scoreBean)->integer+scoreBean.getScore()
,(integer1,integer2)->{
//這個println居然沒有執行!!!
System.out.println("integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
發現這個參數的lambda表達式根本就沒有執行?!
我換了一種方式,換成 parallelStream ,然後把線程id打印出來:
//Integer total=list.stream()
Integer total=list.parallelStream()
.reduce(
Integer.valueOf(0)
,(integer,scoreBean)->integer+scoreBean.getScore()
,(integer1,integer2)->{
//由於用的 parallelStream ,可發生並行計算,所以我增加線程id的打印:
System.out.println("threadId="+Thread.currentThread().getId()+", integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
/*結果如下:
threadId=13, integer1=1, integer2=2
threadId=1, integer1=4, integer2=5
threadId=1, integer1=3, integer2=9
threadId=1, integer1=3, integer2=12
*/
把 stream 換成並行的 parallelStream,
可以看出,有兩個線程在執行任務:線程13和線程1 ,
每個線程會分配幾個元素做計算,
如上面的線程13分配了元素1和2,線程1分配了3、4、5。
至於線程1爲什麼會有兩個3,是由於線程13執行完後得到的結果爲3(1+2),而這個3又會作爲後續線程1的入參進行彙總計算。
可以多跑幾次,每次執行的結果不一定相同,如果看不出來規律,可以嘗試增加集合中的元素個數,數據量大更有利於並行計算發揮作用。
因此,第三個參數 BinaryOperator<U> combiner
的作用爲:彙總所有線程的計算結果得到最終結果。
並行計算會啓動多個線程執行同一個計算任務,每個線程計算完後會有一個結果,最後要將這些結果彙總得到最終結果。
我們再來看一個有意思的結果,把第一個參數 identity 從0換成1:
//Integer total=list.stream()
Integer total=list.parallelStream()
.reduce(
Integer.valueOf(1)
,(integer,scoreBean)->{
System.out.println("$ threadId="+Thread.currentThread().getId()+", integer="+integer+", scoreBean.getScore()="+scoreBean.getScore());
return integer+scoreBean.getScore();
}
,(integer1,integer2)->{
System.out.println("threadId="+Thread.currentThread().getId()+", integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
System.out.println("result="+total);
/* 運行結果如下:
$ threadId=12, integer=1, scoreBean.getScore()=2
$ threadId=1, integer=1, scoreBean.getScore()=3
$ threadId=14, integer=1, scoreBean.getScore()=5
$ threadId=13, integer=1, scoreBean.getScore()=1
$ threadId=15, integer=1, scoreBean.getScore()=4
threadId=13, integer1=2, integer2=3
threadId=15, integer1=5, integer2=6
threadId=15, integer1=4, integer2=11
threadId=15, integer1=5, integer2=15
result=20
*/
預期結果應該是16(初始值1+原來的結果15),但實際結果爲20,多加了4次1,猜測是多加了四次初始值,
從打印的結果可以發現:
(1)並行計算時用了5個線程(線程id依次爲:12, 1, 14, 13, 15),彙總合併時用了兩個線程(線程id爲13和15)
(2)並行計算的每一個線程都用了初始值參與計算,因此多加了4次初始值。
總結:
使用 parallelStream 時,初始值 identity 應該設置一個不影響計算結果的值,比如本示例中設置爲 0 就不會影響結果。
我覺得這個初始值 identity 有兩個作用:確定泛型U的類型
和 避免空指針
。
但是如果初始值本身就是一個複雜對象那該怎麼辦呢?
比如是初始值是一個數組,那麼應該設定爲一個空數組。如果是其他複雜對象那就得根據你reduce的具體含義來設定初始值了。
用表達式來解釋就是初始值identity應該滿足以下等式:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
//combiner.apply(u1,u2) 接收兩個相同類型U的參數
//accumulator.apply(u, t) 接收兩個不同類型的參數U和T,U是返回值的類型,T是集合中元素的類型
//這個等式恆等,parallelStream計算時就不會產生錯誤結果