Warum wird ein Kombinierer für die Reduzierungsmethode benötigt, die den Typ in Java 8 konvertiert?

141

Ich habe Probleme, die Rolle, die combinerdie Streams- reduceMethode erfüllt , vollständig zu verstehen .

Der folgende Code wird beispielsweise nicht kompiliert:

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

Der Kompilierungsfehler lautet: (Argument stimmt nicht überein; int kann nicht in java.lang.String konvertiert werden)

aber dieser Code kompiliert:

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

Ich verstehe, dass die Combiner-Methode in parallelen Streams verwendet wird. In meinem Beispiel addiert sie also zwei akkumulierte Zwischen-Ints.

Aber ich verstehe nicht, warum das erste Beispiel nicht ohne den Kombinierer kompiliert wird oder wie der Kombinierer die Konvertierung von Zeichenfolgen in int löst, da er nur zwei Ints addiert.

Kann jemand Licht ins Dunkel bringen?

Louise Miller
quelle
2
Aha, es ist für parallele Streams ... Ich nenne undichte Abstraktion!
Andy

Antworten:

77

Die Versionen mit zwei und drei Argumenten, reducedie Sie verwendet haben, akzeptieren nicht denselben Typ für die accumulator.

Die beiden Argumente reducesind definiert als :

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

In Ihrem Fall ist T String, BinaryOperator<T>sollte also zwei String-Argumente akzeptieren und einen String zurückgeben. Aber Sie übergeben ihm ein int und einen String, was zu dem Kompilierungsfehler führt, den Sie erhalten haben - argument mismatch; int cannot be converted to java.lang.String. Eigentlich denke ich, dass die Übergabe von 0 als Identitätswert auch hier falsch ist, da ein String erwartet wird (T).

Beachten Sie auch, dass diese Version von redu einen Stream von Ts verarbeitet und ein T zurückgibt, sodass Sie damit keinen Stream von String auf ein int reduzieren können.

Die drei Argumente reducesind definiert als :

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

In Ihrem Fall ist U Integer und T String. Mit dieser Methode wird ein String-Stream auf eine Ganzzahl reduziert.

Für den BiFunction<U,? super T,U>Akkumulator können Sie Parameter von zwei verschiedenen Typen (U und? Super T) übergeben, in Ihrem Fall Integer und String. Außerdem akzeptiert der Identitätswert U in Ihrem Fall eine Ganzzahl, sodass die Übergabe von 0 in Ordnung ist.

Ein anderer Weg, um das zu erreichen, was Sie wollen:

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

Hier stimmt der Typ des Streams mit dem Rückgabetyp von überein reduce, sodass Sie die Zwei-Parameter-Version von verwenden können reduce.

Natürlich müssen Sie überhaupt nicht verwenden reduce:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();
Eran
quelle
8
Als zweite Option in Ihrem letzten Code können Sie auch mapToInt(String::length)over verwenden mapToInt(s -> s.length()), nicht sicher, ob eines besser als das andere ist, aber ich bevorzuge das erstere aus Gründen der Lesbarkeit.
Skiwi
19
Viele werden diese Antwort finden, da sie nicht verstehen, warum das combinerbenötigt wird, warum es nicht accumulatorausreicht, das zu haben. In diesem Fall: Der Kombinierer wird nur für parallele Streams benötigt, um die "akkumulierten" Ergebnisse der Threads zu kombinieren.
Ddekany
1
Ich finde Ihre Antwort nicht besonders nützlich - weil Sie überhaupt nicht erklären, was der Kombinierer tun soll und wie ich ohne sie arbeiten kann! In meinem Fall möchte ich einen Typ T auf ein U reduzieren, aber es gibt keine Möglichkeit, dies jemals parallel zu tun. Es ist einfach nicht möglich. Wie sagt man dem System, dass ich keine Parallelität will / brauche und lasse den Kombinierer aus?
Zordid
@Zordid Die Streams-API enthält keine Option zum Reduzieren des Typs T auf ein U, ohne einen Kombinierer zu übergeben.
Eran
215

Eran Antwort beschrieben , die Unterschiede zwischen den beiden argument und drei argument Versionen reduce, dass der ehemalige reduziert Stream<T>auf Twährend letztere reduziert Stream<T>auf U. Es wurde jedoch nicht die Notwendigkeit der zusätzlichen Kombiniererfunktion beim Reduzieren Stream<T>auf erklärt U.

Eines der Entwurfsprinzipien der Streams-API ist, dass sich die API nicht zwischen sequentiellen und parallelen Streams unterscheiden sollte, oder anders ausgedrückt, eine bestimmte API sollte nicht verhindern, dass ein Stream entweder sequentiell oder parallel korrekt ausgeführt wird. Wenn Ihre Lambdas die richtigen Eigenschaften haben (assoziativ, nicht störend usw.), sollte ein nacheinander oder parallel laufender Stream die gleichen Ergebnisse liefern.

Betrachten wir zunächst die Zwei-Argumente-Version der Reduktion:

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

Die sequentielle Implementierung ist unkompliziert. Der Identitätswert Iwird mit dem nullten Stream-Element "akkumuliert", um ein Ergebnis zu erhalten. Dieses Ergebnis wird mit dem ersten Stream-Element akkumuliert, um ein anderes Ergebnis zu erhalten, das wiederum mit dem zweiten Stream-Element akkumuliert wird, und so weiter. Nachdem das letzte Element akkumuliert wurde, wird das Endergebnis zurückgegeben.

Die parallele Implementierung beginnt mit der Aufteilung des Streams in Segmente. Jedes Segment wird von seinem eigenen Thread in der oben beschriebenen sequentiellen Weise verarbeitet. Wenn wir nun N Threads haben, haben wir N Zwischenergebnisse. Diese müssen auf ein Ergebnis reduziert werden. Da jedes Zwischenergebnis vom Typ T ist und wir mehrere haben, können wir dieselbe Akkumulatorfunktion verwenden, um diese N Zwischenergebnisse auf ein einziges Ergebnis zu reduzieren.

Betrachten wir nun eine hypothetische Zwei-Arg-Reduktionsoperation, die sich Stream<T>auf reduziert U. In anderen Sprachen wird dies als a bezeichnet "Fold" - oder "Fold-Left" -Operation bezeichnet. So werde ich es hier nennen. Beachten Sie, dass dies in Java nicht vorhanden ist.

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

(Beachten Sie, dass der Identitätswert Ivom Typ U ist.)

Die sequentielle Version von foldLeftist genau wie die sequentielle Version vonreduce außer dass die Zwischenwerte vom Typ U anstelle vom Typ T sind. Ansonsten ist es dasselbe. (Eine hypothetische foldRightOperation wäre ähnlich, außer dass die Operationen von rechts nach links statt von links nach rechts ausgeführt würden.)

Betrachten Sie nun die parallele Version von foldLeft . Beginnen wir mit der Aufteilung des Streams in Segmente. Wir können dann jeden der N Threads die T-Werte in seinem Segment in N Zwischenwerte vom Typ U reduzieren lassen. Was nun? Wie kommen wir von N Werten vom Typ U zu einem einzelnen Ergebnis vom Typ U?

Was fehlt , ist eine weitere Funktion, die kombiniert die mehreren Zwischenergebnisse des Typs U zu einem einzigen Ergebnis vom Typ U. Wenn wir eine Funktion , die kombiniert zwei U - Werte in eine, die eine beliebige Anzahl von Werten nach unten auf einen reduzieren ausreichend ist - wie die ursprüngliche Reduktion oben. Daher benötigt die Reduktionsoperation, die ein Ergebnis eines anderen Typs ergibt, zwei Funktionen:

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

Oder mit Java-Syntax:

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

Zusammenfassend benötigen wir für eine parallele Reduktion auf einen anderen Ergebnistyp zwei Funktionen: eine, die T-Elemente auf mittlere U-Werte akkumuliert , und eine zweite, die die die U-Zwischenwerte zu einem einzigen U-Ergebnis kombiniert . Wenn wir nicht zwischen Typen wechseln, stellt sich heraus, dass die Akkumulatorfunktion mit der Kombiniererfunktion identisch ist. Aus diesem Grund hat die Reduktion auf denselben Typ nur die Akkumulatorfunktion, und die Reduktion auf einen anderen Typ erfordert separate Akkumulator- und Kombiniererfunktionen.

Schließlich ist Java nicht bieten foldLeftund foldRightOperationen , weil sie eine bestimmte Reihenfolge von Operationen bedeuten , die von Natur aus sequentiell ist. Dies steht im Widerspruch zu dem oben genannten Entwurfsprinzip, APIs bereitzustellen, die sequentiellen und parallelen Betrieb gleichermaßen unterstützen.

Stuart Marks
quelle
7
Was können Sie also tun, wenn Sie eine benötigen, foldLeftda die Berechnung vom vorherigen Ergebnis abhängt und nicht parallelisiert werden kann?
Amöbe
4
@amoebe Sie können Ihre eigene foldLeft mit implementieren forEachOrdered. Der Zwischenzustand muss jedoch in einer erfassten Variablen gehalten werden.
Stuart Marks
@StuartMarks danke, am Ende habe ich jOOλ verwendet. Sie haben eine ordentliche Implementierung vonfoldLeft .
Amöbe
1
Ich liebe diese Antwort! Korrigieren Sie mich, wenn ich falsch liege: Dies erklärt, warum das laufende Beispiel von OP (das zweite) den Combiner niemals aufruft, wenn er ausgeführt wird und der Stream sequentiell ist.
Luigi Cortese
2
Es erklärt fast alles ... außer: Warum sollte dies eine sequentielle Reduktion ausschließen? In meinem Fall ist es UNMÖGLICH, dies parallel zu tun, da meine Reduktion eine Liste von Funktionen in ein U reduziert, indem jede Funktion für das Zwischenergebnis des Vorgängerergebnisses aufgerufen wird. Dies kann überhaupt nicht parallel erfolgen und es gibt keine Möglichkeit, einen Kombinierer zu beschreiben. Mit welcher Methode kann ich dies erreichen?
Zordid
115

Da ich Kritzeleien und Pfeile mag, um Konzepte zu klären ... fangen wir an!

Von String zu String (sequentieller Stream)

Angenommen, Sie haben 4 Zeichenfolgen: Ihr Ziel ist es, solche Zeichenfolgen zu einer zu verketten. Sie beginnen grundsätzlich mit einem Typ und enden mit demselben Typ.

Sie können dies mit erreichen

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

und dies hilft Ihnen zu visualisieren, was passiert:

Geben Sie hier die Bildbeschreibung ein

Die Akkumulatorfunktion konvertiert die Elemente in Ihrem (roten) Stream Schritt für Schritt in den endgültigen reduzierten (grünen) Wert. Die Akkumulatorfunktion wandelt einfach ein StringObjekt in ein anderes um String.

Von String zu int (paralleler Stream)

Angenommen, Sie haben dieselben 4 Zeichenfolgen: Ihr neues Ziel besteht darin, ihre Längen zu summieren, und Sie möchten Ihren Stream parallelisieren.

Was Sie brauchen, ist ungefähr so:

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

und dies ist ein Schema dessen, was passiert

Geben Sie hier die Bildbeschreibung ein

Hier BiFunctionkönnen Sie mit der Akkumulatorfunktion (a ) Ihre StringDaten in intDaten umwandeln . Da der Stream parallel ist, wird er in zwei (rote) Teile aufgeteilt, von denen jeder unabhängig voneinander ausgearbeitet wird und ebenso viele partielle (orange) Ergebnisse liefert. Das Definieren eines Kombinierers ist erforderlich, um eine Regel zum Zusammenführen von Teilergebnissen intin das endgültige (grüne) Ergebnis bereitzustellen int.

Von String zu int (sequentieller Stream)

Was ist, wenn Sie Ihren Stream nicht parallelisieren möchten? Nun, ein Kombinierer muss sowieso bereitgestellt werden, aber er wird niemals aufgerufen, da keine Teilergebnisse erzeugt werden.

Luigi Cortese
quelle
7
Danke dafür. Ich musste nicht einmal lesen. Ich wünschte, sie hätten gerade eine verdammte Faltfunktion hinzugefügt.
Lodewijk Bogaards
1
@LodewijkBogaards froh, dass es geholfen hat! JavaDoc hier ist in der Tat ziemlich kryptisch
Luigi Cortese
@LuigiCortese Teilt es im parallelen Stream die Elemente immer in Paare?
TheLogicGuy
1
Ich schätze Ihre klare und nützliche Antwort. Ich möchte ein wenig von dem wiederholen, was Sie gesagt haben: "Nun, ein Kombinierer muss trotzdem bereitgestellt werden, aber er wird niemals aufgerufen." Dies ist Teil der funktionalen Programmierung von Brave New World of Java, die, wie mir unzählige Male versichert wurde, "Ihren Code präziser und leichter lesbar macht". Hoffen wir, dass Beispiele für (Fingerzitate) prägnante Klarheit wie diese nur wenige sind.
Dnuttle
Es wird VIEL besser sein, die Reduzierung mit acht Saiten zu veranschaulichen ...
Ekaterina Ivanova iceja.net
0

Es gibt keine reduzierte Version, die zwei verschiedene Typen ohne Kombinierer verwendet, da sie nicht parallel ausgeführt werden kann (nicht sicher, warum dies erforderlich ist). Die Tatsache, dass der Akkumulator assoziativ sein muss, macht diese Schnittstelle ziemlich nutzlos, da:

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

Erzeugt die gleichen Ergebnisse wie:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);
quiz123
quelle
Ein solcher mapTrick hängt von bestimmten ab accumulatorund combinerkann die Dinge ziemlich verlangsamen.
Tagir Valeev
Oder beschleunigen Sie es erheblich, da Sie es jetzt vereinfachen können, accumulatorindem Sie den ersten Parameter löschen.
Quiz123
Eine parallele Reduzierung ist möglich, dies hängt von Ihrer Berechnung ab. In Ihrem Fall müssen Sie sich der Komplexität des Kombinierers, aber auch des Akkumulators der Identität im Vergleich zu anderen Instanzen bewusst sein.
LoganMzz