Ich habe Probleme, die Rolle, die combiner
die Streams- reduce
Methode 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?
java
java-8
java-stream
Louise Miller
quelle
quelle
Antworten:
Die Versionen mit zwei und drei Argumenten,
reduce
die Sie verwendet haben, akzeptieren nicht denselben Typ für dieaccumulator
.Die beiden Argumente
reduce
sind definiert als :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
reduce
sind definiert als :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:
Hier stimmt der Typ des Streams mit dem Rückgabetyp von überein
reduce
, sodass Sie die Zwei-Parameter-Version von verwenden könnenreduce
.Natürlich müssen Sie überhaupt nicht verwenden
reduce
:quelle
mapToInt(String::length)
over verwendenmapToInt(s -> s.length())
, nicht sicher, ob eines besser als das andere ist, aber ich bevorzuge das erstere aus Gründen der Lesbarkeit.combiner
benötigt wird, warum es nichtaccumulator
ausreicht, das zu haben. In diesem Fall: Der Kombinierer wird nur für parallele Streams benötigt, um die "akkumulierten" Ergebnisse der Threads zu kombinieren.Eran Antwort beschrieben , die Unterschiede zwischen den beiden argument und drei argument Versionen
reduce
, dass der ehemalige reduziertStream<T>
aufT
während letztere reduziertStream<T>
aufU
. Es wurde jedoch nicht die Notwendigkeit der zusätzlichen Kombiniererfunktion beim ReduzierenStream<T>
auf erklärtU
.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:
Die sequentielle Implementierung ist unkompliziert. Der Identitätswert
I
wird 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 reduziertU
. 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.(Beachten Sie, dass der Identitätswert
I
vom Typ U ist.)Die sequentielle Version von
foldLeft
ist genau wie die sequentielle Version vonreduce
außer dass die Zwischenwerte vom Typ U anstelle vom Typ T sind. Ansonsten ist es dasselbe. (Eine hypothetischefoldRight
Operation 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:
Oder mit Java-Syntax:
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
foldLeft
undfoldRight
Operationen , 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.quelle
foldLeft
da die Berechnung vom vorherigen Ergebnis abhängt und nicht parallelisiert werden kann?forEachOrdered
. Der Zwischenzustand muss jedoch in einer erfassten Variablen gehalten werden.foldLeft
.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
und dies hilft Ihnen zu visualisieren, was passiert:
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
String
Objekt in ein anderes umString
.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:
und dies ist ein Schema dessen, was passiert
Hier
BiFunction
können Sie mit der Akkumulatorfunktion (a ) IhreString
Daten inint
Daten 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 Teilergebnissenint
in das endgültige (grüne) Ergebnis bereitzustellenint
.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.
quelle
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:
Erzeugt die gleichen Ergebnisse wie:
quelle
map
Trick hängt von bestimmten abaccumulator
undcombiner
kann die Dinge ziemlich verlangsamen.accumulator
indem Sie den ersten Parameter löschen.