java8集合Stream之reduce聚合函數——看這篇就夠了

說明

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計算時就不會產生錯誤結果










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章