Java 8で型を変換するreduceメソッドにコンバイナーが必要なのはなぜですか


141

combinerStreams reduceメソッドで果たす役割を十分に理解できません。

たとえば、次のコードはコンパイルされません。

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);

私はコンバイナーメソッドが並列ストリームで使用されていることを理解しています。つまり、私の例では、2つの中間累積整数を加算しています。

しかし、最初の例がコンバイナなしでコンパイルできない理由や、2つのintを加算するだけなので、コンバイナが文字列からintへの変換をどのように解決するのか理解できません。

誰かがこれに光を当てることができますか?



2
ああ、それは並列ストリーム用です...私はリーキーアブストラクションと呼んでいます!
アンディ

回答:


77

reduce使用しようとした2つおよび3つの引数のバージョンは、の同じタイプを受け入れませんaccumulator

2つの引数reduce次のように定義されます。

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

あなたの場合、Tは文字列なのでBinaryOperator<T>、2つの文字列引数を受け入れ、文字列を返す必要があります。しかし、それにintとStringを渡すと、コンパイルエラーが発生します-argument mismatch; int cannot be converted to java.lang.String。実際、Stringが期待されているため(T)、ID値として0を渡すこともここでは間違っていると思います。

また、このバージョンのreduceはTのストリームを処理してTを返すため、これを使用してStringのストリームをintに縮小することはできません。

3つの引数reduce次のように定義されます。

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

あなたの場合、Uは整数でTは文字列なので、このメソッドは文字列のストリームを整数に減らします。

以下のためにBiFunction<U,? super T,U>アキュムレータあなたの場合には整数と文字列を2つの異なるタイプ(Uと?スーパーT)、のパラメータを渡すことができます。さらに、アイデンティティ値UはあなたのケースではIntegerを受け入れるので、0を渡しても問題ありません。

あなたが望むものを達成する別の方法:

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

ここでは、ストリームのタイプがの戻り値のタイプと一致reduceするため、の2つのパラメーターバージョンを使用できますreduce

もちろん、あなたはまったく使用する必要はありませんreduce

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

8
最後のコードの2番目のオプションとして、mapToInt(String::length)overを使用することもできます。mapToInt(s -> s.length())一方が他方よりも優れているかどうかはわかりませんが、読みやすさの点で前者を優先します。
skiwi、2014年

19
彼らがなぜcombiner必要なのか、なぜaccumulator十分でないのかわからないので、多くはこの答えを見つけるでしょう。その場合:コンバイナは、スレッドの「累積された」結果を組み合わせるために、並列ストリームにのみ必要です。
ddekany 2017年

1
あなたの答えは特に役に立ちません-コンバイナが何をすべきか、そしてそれなしで私がどのように働くことができるかをあなたはまったく説明しないからです!私の場合、タイプTをUに減らしたいのですが、これを並列処理する方法はまったくありません。それは単に不可能です。並列処理をしたくない、または必要としないシステムに、コンバイナを除外するにはどうすればよいですか?
Zordid

@Zordid Streams APIには、コンバイナを渡さずにタイプTをUに減らすオプションは含まれていません。
エラン2018

216

Eranの回答は、reduce前者がに減少Stream<T>するのTに対し、後者はに減少Stream<T>するという2引数バージョンと3引数バージョンの違いを説明していUます。ただし、に削減Stream<T>する場合の追加のコンバイナ機能の必要性は実際には説明されていませんU

Streams APIの設計原則の1つは、APIがシーケンシャルストリームとパラレルストリームで異なっていてはならない、言い換えると、特定のAPIがストリームのシーケンシャルまたはパラレルの正常な実行を妨げないことです。ラムダに適切なプロパティ(連想、非干渉など)がある場合、順次または並列に実行されるストリームは同じ結果をもたらすはずです。

まず、2引数バージョンのリダクションについて考えてみましょう。

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

順次実装は簡単です。ID値Iは、結果を与えるために、0番目のストリーム要素で「累積」されます。この結果は、最初のストリーム要素と累積されて別の結果が得られ、次に2番目のストリーム要素と累積されます。最後の要素が蓄積された後、最終結果が返されます。

並列実装は、ストリームをセグメントに分割することから始まります。各セグメントは、前述の順次方式で独自のスレッドによって処理されます。ここで、N個のスレッドがある場合、N個の中間結果があります。これらは、1つの結果に減らす必要があります。各中間結果はタイプTであり、いくつかあるため、同じアキュムレーター関数を使用して、それらのN個の中間結果を単一の結果に減らすことができます。

今度は、削減仮想の2-argを低減動作考えるStream<T>にはU。他の言語では、これは「フォールド」または「フォールドレフト」操作と呼ばれるため、ここではそれを呼び出します。これはJavaには存在しないことに注意してください。

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

(ID値IはタイプUであることに注意してください。)

の順次バージョンは、中間値がタイプTではなくタイプUであることfoldLeftreduce除いて、順次バージョンのと同じですが、それ以外は同じです。(仮想的なfoldRight操作は、操作が左から右ではなく右から左に実行されることを除いて、類似しています。)

次に、の並列バージョンを考えfoldLeftます。ストリームをセグメントに分割することから始めましょう。次に、N個のスレッドのそれぞれに、そのセグメントのT値をU型のN個の中間値に減らすことができます。U型のN個の値からU型の単一の結果にどのようにして到達するのでしょうか?

不足しているのは、タイプUの複数の中間結果をタイプUの単一の結果に結合する別の関数です。2つのU値を1つに結合する関数がある場合、これは、任意の数の値を1に減らすのに十分です-同様に上記の元の削減。したがって、異なるタイプの結果を与えるリダクション操作には、2つの関数が必要です。

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

または、Java構文を使用します。

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

要約すると、異なる結果タイプに並列削減を行うには、2つの関数が必要です。1つはT要素を中間U値に累積し、もう1つは中間U値を単一のU結果に結合します。タイプを切り替えない場合、アキュムレータ関数はコンバイナ関数と同じであることがわかります。そのため、同じタイプへの削減にはアキュムレータ関数のみがあり、別のタイプへの削減には、別々のアキュムレータおよびコンバイナ関数が必要です。

最後に、Javaが提供していないfoldLeftfoldRight、彼らは本質的にシーケンシャルである操作の特定の順序を暗示するので操作。これは、シーケンシャル操作とパラレル操作を同等にサポートするAPIを提供するという前述の設計原則と衝突します。


7
ではfoldLeft、計算が前の結果に依存し、並列化できないために必要な場合はどうすればよいでしょうか。
amoebe

5
@amoebeを使用して独自のfoldLeftを実装できますforEachOrdered。ただし、中間状態はキャプチャされた変数に保持する必要があります。
スチュアートマーク、

@StuartMarksのおかげで、結局jOOλを使用することになりました。彼らはきちんと実装されていfoldLeftます。
amoebe

1
この答えが大好きです!私が間違っている場合は修正してください。これは、OPの実行例(2番目の例)が実行時にストリームシーケンシャルであるコンバイナを呼び出さない理由を説明しています。
Luigi Cortese 2015年

2
それはほとんどすべてを説明します...を除いて:なぜこれは逐次ベースの削減を除外する必要があるのですか?私の場合、前の結果の中間結果で各関数を呼び出すことによって関数のリストをUに削減するため、並列に実行することは不可能です。これはまったく並行して行うことはできず、コンバイナを記述する方法はありません。これを達成するためにどのような方法を使用できますか?
Zordid

115

コンセプトを明確にするために落書きと矢印が好きなので、始めましょう!

文字列から文字列へ(順次ストリーム)

4つの文字列があるとします。あなたの目標は、そのような文字列を1つに連結することです。基本的にタイプから始めて、同じタイプで終わります。

あなたはこれを達成することができます

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

これは何が起こっているのかを視覚化するのに役立ちます:

ここに画像の説明を入力してください

アキュムレータ関数は、(赤)ストリームの要素を段階的に、最終的に削減された(緑)値に変換します。アキュムレータ関数は、単にStringオブジェクトを別のに変換しますString

Stringからint(並列ストリーム)へ

同じ4つの文字列があるとします。新しい目標はそれらの長さを合計することであり、ストリームを並列化する必要があります。

必要なものは次のようなものです:

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

そしてこれは何が起こっているかの計画です

ここに画像の説明を入力してください

ここでアキュムレータ関数(a BiFunction)を使用すると、Stringデータをデータに変換できますint。ストリームは並列であるため、2つの部分(赤)に分割されます。各部分は互いに独立して生成され、同じくらい多くの部分(オレンジ)結果を生成します。部分的なint結果を最終的な(緑の)結果にマージするためのルールを提供するには、コンバイナを定義する必要がありますint

Stringからint(シーケンシャルストリーム)へ

ストリームを並列化しない場合はどうなりますか?とにかく、コンバイナを提供する必要がありますが、部分的な結果が生成されないので、コンバイナが呼び出されることはありません。


7
これをありがとう。私も読む必要はありませんでした。彼らがひどい折り畳み機能を追加してくれればいいのにと思います。
Lodewijk Bogaards 2016年

1
@LodewijkBogaards助けてくれて嬉しいです!ここのJavaDocは実にかなり不可解です
Luigi Cortese

@LuigiCortese並列ストリームでは、要素を常にペアに分割しますか?
TheLogicGuy 2017年

1
明確で有用な回答をいただければ幸いです。「さてとにかく、コンバイナを提供する必要がありますが、呼び出されることはありません。」これはJava関数プログラミングのBrave New Worldの一部であり、「コードがより簡潔で読みやすくなる」と何度も保証されてきました。(指の引用符)の例が、このような簡潔さを明確にしていることを期待してみましょう。
-dnuttle

8つの弦でリデュースを説明すると、
はるかに

0

並列に実行できないため、コンバイナなしで2つの異なるタイプを使用するリデュースバージョンはありません(これが要件である理由がわかりません)。アキュムレータは結合的でなければならないという事実により、このインターフェースはほとんど役に立たなくなります。

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

同じ結果が生成されます。

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

このようなmapトリック特定に依存accumulatorし、combinerかなり多くの物事が遅くなる場合があります。
Tagir Valeev

または、accumulator最初のパラメーターを削除することで簡略化できるため、大幅にスピードアップします。
quiz123 2015

並列リダクションは可能ですが、計算によって異なります。あなたのケースでは、コンバイナーの複雑さを認識する必要がありますが、IDと他のインスタンスのアキュムレーターも認識している必要があります。
LoganMzz 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.