2014-06-19 45 views
70

我無法完全理解combiner在Streams reduce方法中實現的角色。爲什麼需要使用組合器來轉換Java中的類型8

例如,下面的代碼編譯犯規:

int length = asList("str1", "str2").stream() 
      .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length()); 

編譯錯誤說: (參數不匹配; INT不能被轉換爲java.lang.String)

但這個代碼編譯:

int length = asList("str1", "str2").stream() 
    .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length(), 
       (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); 

我知道組​​合器方法是用於並行流 - 所以在我的示例中,它將兩個中間累積整數相加。

但我不明白爲什麼第一個例子不編譯沒有組合器或組合器如何解決字符串轉換爲int,因爲它只是加在一起兩個整數。

任何人都可以闡明這一點嗎?

+0

相關問題:https://stackoverflow.com/questions/24202473/does-a-sequential-stream-in-java-8-use-the-combiner-parameter-on-calling-collect – nosid

+1

啊哈,這是爲並行流......我稱之爲泄漏抽象! – Andy

回答

45

您嘗試使用的兩個和三個參數版本reduce不接受accumulator的相同類型。

兩個參數reducedefined as

T reduce(T identity, 
     BinaryOperator<T> accumulator) 

在你的情況下,T爲String,所以BinaryOperator<T>應該接受兩個字符串參數,並返回一個字符串。但是你傳遞給它一個int和一個String,這會導致編譯錯誤 - argument mismatch; int cannot be converted to java.lang.String。實際上,我認爲傳遞0作爲身份值在這裏也是錯誤的,因爲字符串是預期的(T)。

另請注意,此版本的reduce會處理Ts流並返回T,因此您無法使用它將String流減少爲int。

三個參數reducedefined as

<U> U reduce(U identity, 
      BiFunction<U,? super T,U> accumulator, 
      BinaryOperator<U> combiner) 

在你的情況U是整數,T是字符串,所以這種方法將字符串流減小到一個整數。

對於BiFunction<U,? super T,U>累加器,您可以傳遞兩種不同類型的參數(U和?super T),在您的情況下它們是Integer和String。另外,身份值U在你的情況下接受一個I​​nteger,所以傳遞它0就可以了。

另一種方式來實現你想要的:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) 
      .reduce(0, (accumulatedInt, len) -> accumulatedInt + len); 

這裏流的類型的reduce的返回類型相匹配,這樣你就可以使用reduce兩個參數的版本。

當然,你不必使用reduce都:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) 
      .sum(); 
+7

作爲最後一個代碼的第二個選項,你也可以在'mapToInt(s - > s.length())'上使用'mapToInt(String :: length)',不確定一個會比另一個更好,但是我更喜歡前者的可讀性。 – skiwi

+2

許多人會發現這個答案,因爲他們不明白爲什麼需要'combiner',爲什麼不用'accumulator'就足夠了。在這種情況下:組合器僅用於並行流,以結合線程的「累積」結果。 – ddekany

109

Eran's answer描述的兩ARG和前者的reduce三ARG版本之間的差異減少Stream<T>T而後者將Stream<T>減少爲U。但是,在將Stream<T>減小爲U時,實際上並未解釋需要額外的組合器功能。

Streams API的設計原則之一是,API不應該在順序流和並行流之間不同,或者換句話說,特定的API不應該阻止順序或並行正確運行流。如果您的lambda具有正確的屬性(關聯,不干擾等),則順序運行或並行運行的流應該得到相同的結果。

我們先考慮減少兩ARG版本:

T reduce(I, (T, T) -> T) 

順序實現非常簡單。標識值I與第零個流元素「累積」以給出結果。該結果與第一個流元素一起累加以給出另一個結果,該結果又與第二個流元素一起累積,等等。最後一個元素累積後,返回最終結果。

並行實現通過將流拆分爲段開始。每段都以自己的線程按照上面描述的順序方式進行處理。現在,如果我們有N個線程,我們有N箇中間結果。這些需要減少到一個結果。由於每個中間結果都是T型的,並且我們有幾個,我們可以使用相同的累加器函數將這N箇中間結果減少到單個結果。

現在讓我們考慮一個假設的雙參數歸約操作,將Stream<T>減小爲U。在其他語言中,這被稱爲「摺疊」或「摺疊式」操作,所以這就是我在這裏所說的。注意這在Java中不存在。

U foldLeft(I, (U, T) -> U) 

(注意,標識值是I類型U的)

foldLeft的順序版本就像的reduce除了順序版本,所述中間值是U型的,而不是T類型的但其他方面是一樣的。 (假設的foldRight操作將類​​似,但操作將從右到左而不是從左到右執行。)

現在考慮foldLeft的並行版本。我們首先將流分成多個段。然後,我們可以讓N個線程中的每一個線程將其段中的T值縮減爲N個U類型的中間值。現在呢?我們如何從U型的N個值到U型的單個結果?

現在缺少的是另外一個新功能結合 U型的多重中間成果轉化型U的一個結果。如果我們有一個結合了兩個U值到一個函數,這是足以降低任何數量的值下降到一個 - 就像上面的原始減少。因此,減少操作,給出了一個不同類型的結果需要兩個功能:

U reduce(I, (U, T) -> U, (U, U) -> U) 

或者,使用Java語法:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) 

總之,做平行減少到一個不同的結果類型,我們需要兩個函數:一個積累 T元素到中間U值,第二個結合中間U值到一個單一的U結果。如果我們不切換類型,則會發現累加器功能與組合器功能相同。這就是爲什麼減少到相同類型只有累加器功能,減少到不同類型需要單獨的累加器和組合器功能。

最後,Java不提供foldLeftfoldRight操作,因爲它們意味着固有順序操作的特定順序。這與上面提到的提供支持順序和並行操作的API的設計原則相沖突。

+4

plus1這使事情變得很清楚,謝謝! – naikus

+6

那麼,如果你需要'foldLeft',你能做什麼,因爲計算取決於以前的結果,不能並行化? – amoebe

+2

@amoebe你可以使用'forEachOrdered'來實現自己的foldLeft。不過,中間狀態必須保存在捕獲的變量中。 –

0

沒有減少版本,有兩種不同類型的無組合,因爲它不能並行執行(不知道爲什麼這是一個要求)。這累加器必須是關聯的事實使得這種接口幾乎無用的,因爲:

list.stream().reduce(identity, 
        accumulator, 
        combiner); 

產生相同的結果:

list.stream().map(i -> accumulator(identity, i)) 
      .reduce(identity, 
        combiner); 
+0

這種'map'技巧取決於特定的'accumulator'和'combiner'可能會讓事情變得非常緩慢。 –

+0

或者,由於您現在可以通過刪除第一個參數來簡化「累加器」,因此顯着加快了速度。 – quiz123

+0

並行減少是可能的,這取決於你的計算。在你的情況下,你必須意識到組合器的複雜性,但也要注意身份與其他實例的累加器。 – LoganMzz

57

因爲我喜歡塗鴉和箭頭澄清概念...讓我們開始!

從串來串(連續流)

假設有4個字符串:你的目標是這樣的字符串連接成一個。你基本上是從一個類型開始,並完成相同的類型。

您可以

String res = Arrays.asList("one", "two","three","four") 
     .stream() 
     .reduce("", 
       (accumulatedStr, str) -> accumulatedStr + str); //accumulator 

實現這一點,這可以幫助你想象發生的事情:

enter image description here

累加器功能轉換,分步實施,在你(紅色)的元素流到最終減少的(綠色)值。累加器功能簡單地將String對象轉換爲另一個String

從字符串到INT(平行流)

假設具有相同的四根弦:你的新目標是要總結自己的長度,且要並行化流。

你需要的是這樣的:

int length = Arrays.asList("one", "two","three","four") 
     .parallelStream() 
     .reduce(0, 
       (accumulatedInt, str) -> accumulatedInt + str.length(),     //accumulator 
       (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner 

,這是發生了什麼事

enter image description here

這裏蓄能器功能的方案(一BiFunction)可以讓你改變你的String數據到一個int數據。作爲平行流,它被分成兩個(紅色)部分,每個部分都是相互獨立的,併產生同樣多的部分(橙色)結果。需要定義組合器來提供將部分int結果合併到最終(綠色)int之一的規則。

從字符串到INT(連續流)

如果你不想你的並行流?那麼,無論如何都需要提供一個組合器,但由於不會產生部分結果,所以它將永遠不會被調用。

+2

感謝您的支持。我甚至不需要閱讀。我希望他們會添加一個怪胎摺疊功能。 –

+0

@LodewijkBogaards很高興幫助! [JavaDoc的](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-U-java.util.function.BiFunction-java.util.function。 BinaryOperator-)這裏是相當神祕的 –

+0

我喜歡你的解釋.. 謝謝.. – AnkitRox

相關問題