在一起來學Java8(七)——Stream(中)
我們學習了Stream.collect
的用法段誊,今天我們來學習下Stream.reduce
的用法猪钮。
reduce操作可以理解成對Stream中元素累計處理廊蜒,它有三個重載方法枣宫。
- 重載1:
Optional<T> reduce(BinaryOperator<T> accumulator);
- 重載2:
T reduce(T identity, BinaryOperator<T> accumulator);
- 重載3:
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
reduce(accumulator)
先來看下重載1方法夭织,這個方法需要我們傳入一個參數(shù)赌结,參數(shù)名字定義為收集器
捞蛋,顧名思義是需要我們對元素進行收集。
下面通過一個數(shù)值累加的例子來說明reduce
的基本用法柬姚。
Optional<Integer> opt = Stream.of(1, 2, 3, 4)
.reduce((n1, n2) -> {
int ret = n1 + n2;
System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
return ret;
});
int sum = opt.orElse(0);
System.out.println("sum:" + sum);
打幽馍肌:
1(n1) + 2(n2) = 3
3(n1) + 3(n2) = 6
6(n1) + 4(n2) = 10
sum:10
這個例子中對Stream例子中的三個數(shù)字進行相加,得到總和量承。最后返回一個Optional<Integer>
對象是因為考慮到Stream中沒有元素的情況捣域,因此返回結(jié)果是未知的霞揉,應該由開發(fā)者來確定返回值酝惧。
在Lambda表達式中提供了兩個參數(shù)n1,n2。從打印結(jié)果中可以看出担孔,n1,n2最開始分別是Stream中第一卦洽,第二兩個元素贞言,把這兩個數(shù)進行相加后返回,然后帶著這個結(jié)果再次進入到Lambda表達式中阀蒂,n1是前一次相加后的值该窗,n2是下一個元素值弟蚀。
這段代碼效果等同于:
int[] arr = { 1, 2, 3, 4};
int sum = 0;
for (int i : arr) {
sum += i;
}
reduce(identity, accumulator, combiner)
再來看下重載3,這個方法有三個參數(shù)酗失,每個參數(shù)說明如下:
- identity:給定一個初始值
- accumulator:基于初始值义钉,對元素進行收集歸納
- combiner:對每個accumulator返回的結(jié)果進行合并,此參數(shù)只有在并行模式中生效规肴。
如果Stream對象是串行的捶闸,那么只有accumulator生效,combiner是不生效的拖刃。
使用Stream.parallel()
的方法開啟并行模式删壮,使用Stream.sequential()
開啟串行模式,默認開啟的是串行模式兑牡。
并行模式可以簡單理解為在多線程中執(zhí)行央碟,每個線程中單獨執(zhí)行它的任務。串行則是在單一線程中順序執(zhí)行均函。
下面來看下重載3的例子:
int sum = Stream.of(1, 2, 3, 4)
.reduce(0, (n1, n2) -> {
int ret = n1 + n2;
System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
return ret;
}, (s1, s2) -> {
int ret = s1 + s2;
System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
return ret;
});
System.out.println("sum:" + sum);
打右谒洹:
0(n1) + 1(n2) = 1
1(n1) + 2(n2) = 3
3(n1) + 3(n2) = 6
6(n1) + 4(n2) = 10
sum:10
可以看到,在串行模式下并沒運行combiner參數(shù)苞也,只運行了accumulator參數(shù)洛勉,從給定的初始值0開始累加。
這里已經(jīng)指定了初始值(identity)墩朦,因此返回類型就是初始值的類型坯认。
我們把例子改成并行模式翻擒,然后看下執(zhí)行結(jié)果氓涣。
int sum = Stream.of(1, 2, 3, 4)
.parallel() // 并行模式
.reduce(0, (n1, n2) -> {
int ret = n1 + n2;
System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
return ret;
}, (s1, s2) -> {
int ret = s1 + s2;
System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
return ret;
});
System.out.println("sum:" + sum);
打印:
0(n1) + 3(n2) = 3
0(n1) + 1(n2) = 1
0(n1) + 2(n2) = 2
1(s1) + 2(s2) = 3
0(n1) + 4(n2) = 4
3(s1) + 4(s2) = 7
3(s1) + 7(s2) = 10
sum:10
從打印的結(jié)果中我們可以看到幾個現(xiàn)象:
- combiner參數(shù)被執(zhí)行了
- 打印的內(nèi)容是無序的陋气,說明它們在多線程環(huán)境下執(zhí)行的
-
n1
參數(shù)始終是0
因為是并行模式劳吠,前2個現(xiàn)象很好理解,那為什么n1
參數(shù)始終是0巩趁?
因為開了并行模式后痒玩,運行reduce方法的底層是使用了ForkJoinPool
(分支/合并框架)。
分支/合并框架
的原理是將一個大任務拆分成多個子任務议慰,這些子任務并行處理自己的事情蠢古,然后框架將這些子任務的結(jié)果合并起來,生成一個最終結(jié)果别凹。
每個子任務之間是沒有關聯(lián)的草讶,它們的執(zhí)行狀態(tài)都是一樣的,因此每個子任務給到的初始值(identity)都是一樣的炉菲,在本例中是0
同時需要一個合并方法用來合并每個子任務的處理結(jié)果堕战,然后最終返回坤溃,使用數(shù)學表達式即為:
(0+1) + (0+2) + (0+3) + (0+4) = 10
再來看下重載3這個方法簽名,每個參數(shù)的分工都明確了嘱丢。
reduce(identity, accumulator, combiner)
- identity:初始值
- accumulator:每個子任務執(zhí)行的操作
- combiner:合并每個子任務的結(jié)果
注意事項
查看reduce方法文檔薪介,發(fā)現(xiàn)有下面一段話:
Performs a reduction on the elements of this stream,
using the provided identity value and an associative accumulation function,
and returns the reduced value. This is equivalent to:
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
The identity value must be an identity for the accumulator function.
This means that for all t, accumulator.apply(identity, t) is equal to t.
The accumulator function must be an associative function.
其中有一句重要的話:This means that for all t, accumulator.apply(identity, t) is equal to t.
簡單來說,必須要滿足下面這個公式:
accumulator.apply(identity, t) == t
如果不滿足的話越驻,在并行模式下執(zhí)行accumulator會有問題汁政。
我們把上一個例子中的初始值改成1,然后看看執(zhí)行結(jié)果
int sum = Stream.of(1, 2, 3, 4)
.parallel()
// 這里改成了1
.reduce(1, (n1, n2) -> {
int ret = n1 + n2;
System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
return ret;
}, (s1, s2) -> {
int ret = s1 + s2;
System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
return ret;
});
System.out.println("sum:" + sum);
打臃ヌ浮:
1(n1) + 3(n2) = 4
1(n1) + 4(n2) = 5
4(s1) + 5(s2) = 9
1(n1) + 2(n2) = 3
1(n1) + 1(n2) = 2
2(s1) + 3(s2) = 5
5(s1) + 9(s2) = 14
sum:14
理想中的結(jié)果應該是11才對烂完,即1 + 1 + 2 + 3 + 4
∷锌茫可以看到在并行模式下對identity的值是有要求的抠蚣。
必須滿足公式:accumulator.apply(identity, t) == t
。
這里accumulator.apply(identity, t) == t
即為:accumulator.apply(1, 1) == 1
履澳,使用數(shù)學表達式表示:
1(identity) + 1 == 1
顯然這個等式是不成立的嘶窄,把identity改成0則公式成立:0 + 1 == 1
緊接著,對于combiner參數(shù)距贷,需要滿足另一個公式:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
- t:表示第一個參數(shù)
- u:表示第二個參數(shù)
在這個例子中柄冲,我們?nèi)〉谝淮螆?zhí)行combiner情況: 4(s1) + 5(s2) = 9
,套用公式即為:
combiner.apply(5, accumulator.apply(1, 4)) == accumulator.apply(5, 4)
在這里u=5忠蝗,identity=1现横,t=4
轉(zhuǎn)換成數(shù)學表達式為:5 + (1 + 4) == 5 + 4
顯然這個等式是不成立的,把identity改成0阁最,等式就成立了:5 + (0 + 4) == 5 + 4
總結(jié)一下
使用reduce(identity, accumulator, combiner)
方法時戒祠,必須同時滿足下面兩個公式:
- 公式1,針對accumulator:
accumulator.apply(identity, t) == t
- 公式2速种,針對combiner:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
reduce(identity, accumulator)
這個方法其實是reduce(identity, accumulator, combiner)的一種特殊形式姜盈,只不過是把combiner部分用accumulator來代替了,即
reduce(identity, accumulator)
等同于reduce(identity, accumulator, accumulator)
因此reduce(identity, accumulator)
的使用方式和注意事項是跟reduce(identity, accumulator, combiner)
一樣的配阵,這里不再贅述馏颂。
小節(jié)
本篇主要講解了Stream.reduce的使用方法及注意事項,在并行模式下棋傍,reduce是使用分支/合并框架
實現(xiàn)的救拉,在下一篇文章中我們開始學習分支/合并框架
。
定期分享技術干貨瘫拣,一起學習亿絮,一起進步!微信公眾號:猿敲月下碼