一起來學Java8(七)——Stream(下)

一起來學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)象:

  1. combiner參數(shù)被執(zhí)行了
  2. 打印的內(nèi)容是無序的陋气,說明它們在多線程環(huán)境下執(zhí)行的
  3. 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)的救拉,在下一篇文章中我們開始學習分支/合并框架

定期分享技術干貨瘫拣,一起學習亿絮,一起進步!微信公眾號:猿敲月下碼

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市壹无,隨后出現(xiàn)的幾起案子葱绒,更是在濱河造成了極大的恐慌,老刑警劉巖斗锭,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件地淀,死亡現(xiàn)場離奇詭異,居然都是意外死亡岖是,警方通過查閱死者的電腦和手機帮毁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豺撑,“玉大人烈疚,你說我怎么就攤上這事〈辖危” “怎么了爷肝?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長陆错。 經(jīng)常有香客問我灯抛,道長,這世上最難降的妖魔是什么音瓷? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任对嚼,我火速辦了婚禮,結(jié)果婚禮上绳慎,老公的妹妹穿的比我還像新娘纵竖。我一直安慰自己,他們只是感情好杏愤,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布靡砌。 她就那樣靜靜地躺著,像睡著了一般声邦。 火紅的嫁衣襯著肌膚如雪乏奥。 梳的紋絲不亂的頭發(fā)上摆舟,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天亥曹,我揣著相機與錄音,去河邊找鬼恨诱。 笑死媳瞪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的照宝。 我是一名探鬼主播蛇受,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼厕鹃!你這毒婦竟也來了兢仰?” 一聲冷哼從身側(cè)響起乍丈,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎把将,沒想到半個月后轻专,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡察蹲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年请垛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洽议。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡宗收,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亚兄,到底是詐尸還是另有隱情混稽,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布审胚,位于F島的核電站荚坞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏菲盾。R本人自食惡果不足惜颓影,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望懒鉴。 院中可真熱鬧诡挂,春花似錦、人聲如沸临谱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悉默。三九已至城豁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抄课,已是汗流浹背唱星。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跟磨,地道東北人间聊。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像抵拘,于是被迫代替她去往敵國和親哎榴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容