Transformationen gefällig ? – reduce the stream

Java 8 Streams: Core Methods Teil 3

Sven Ruppert
©Shutterstock.com/ilolab

Wie transformiere ich einen Stream aus n Elementen auf ein finales Ergebnis? Was passiert bei seriellen, was bei parallelen Streams? Es ist einfacher als angenommen..

Bei den bisherigen Betrachtungen wurden ausschließlich Transformationen betrachtet, die eine Abbildung von n auf m darstellten. Die Methode reduce((v1,v2)->) jedoch ermöglicht die Abbildung von n Elementen auf ein finales Element. Hierbei liegt das besondere Augenmerk auf dem unterschiedlichen Verhalten bei seriellen und parallelen Streams.

reduce – verbinde und reduziere

Alle bisher betrachteten Methoden waren nicht in der Lage, z.B. Elemente der Position n-1 in die Verwertung des Elements n mit einzubeziehen. Wie also kann man aufeinander aufbauende Werte erzeugen? Nehmen wir als Beispiel das immer zu dem Wert n der Wert n-1 angehängt werden soll. Die Eingangswerte sind die Zeichen der Kette „A, B, C, D, E“. Diese Elemente sollen verbunden werden. Listing 1 zeigt die erste Version einmal mit seriellem und mit parallelem Stream. Die Methode reduce((v1,v2)->) bekommt ein Lambda mit zwei Parametern. V1 und V2, deren Inhalt aus dem Stream die Elemente n-1 und n sind. Bei beiden Versionen kommt dieselbe Zeichenfolge „ABCDE“ verpackt in einem Optional heraus. Das ist verwunderlich, da doch die zweite Version ein paralleler Stream ist.

final List<String> demoValues = Arrays.asList("A", "B", "C", "D", "E");
    System.out.println(demoValues.stream()
    .reduce(String::concat)); //Optional[ABCDE]

    System.out.println(demoValues.parallelStream()
    .reduce(String::concat)); //Optional[ABCDE]

Ändern wir also die Implementierung aus Listing 1 zu der Implementierung Listing 2 um ein wenig mehr zu sehen. Das Ergebnis ist nun für die serielle Version X_ABCDE und für die parallele Version X_AX_BX_CX_DX_E_. Nun stellt sich die Frage in welchen Teilschritten werden also die jeweiligen Ergebnisse produziert (Listing 3)? Dazu erweitern wir ein wenig die Ausgabe.

 final List<String> demoValues = Arrays.asList("A", "B", "C", "D", "E");
    //result is X_ABCDE
    System.out.println(demoValues.stream()
        .reduce("X_", String::concat));

    //result is X_AX_BX_CX_DX_E
    System.out.println(demoValues.parallelStream()
        .reduce("X_", String::concat));

Die Ausgabe wird nun mit einem Postfix erweitert. Nun kann man einfach die einzelnen Schritte erkennen, die im Falle des parallelen Streams erfolgen. Die serielle Version ergibt den String X_A_B_C_D_E_. Hier wird also der Prefix, dann jedes Element mit dem „_“ konkateniert. Alles ist in der Reihenfolge wie es im Quellstream vorhanden ist. Bei der parallelen Version sieht das Ergebnis gänzlich anders aus. X_A_X_B__X_C_X_D_X_E____ (Am Ende sind vier Unterstriche, hinter dem B sind zwei Unterstriche). Hier lohnt es sich die einzelnen Schritte genauer anzusehen (Listing 4). Wichtig zu wissen ist, dass bei jedem Durchlauf Listing 4 anders aussehen wird. Die Schritte bleiben jedoch dieselben, genau wie das Ergebnis auch.

final List<String> demoValues = Arrays.asList("A", "B", "C", "D", "E");
    System.out.println(demoValues.stream()
        .reduce("X_", (v1,v2)- >{
            System.out.println("v1 -> " + v1);
            System.out.println("v2 -> " + v2);
            return v1.concat(v2)+"_";
    }));

    System.out.println(demoValues.parallelStream()
        .reduce("X_", (v1,v2)- >{
            System.out.println("v1 -> " + v1);
            System.out.println("v2 -> " + v2);
            return v1.concat(v2)+"_";
    }));
 v1 -> X_
    v1 -> X_
    v2 -> D
    v1 -> X_
    v2 -> E
    v1 -> X_D_
    v2 -> X_E_
    v1 -> X_
    v2 -> B
    v1 -> X_
    v2 -> A
    v2 -> C
    v1 -> X_A_
    v2 -> X_B_
    v1 -> X_C_
    v2 -> X_D_X_E__
    v1 -> X_A_X_B__
    v2 -> X_C_X_D_X_E___
    X_A_X_B__X_C_X_D_X_E____

Verfolgt man die einzelnen Schritte, so erkennt man die Aufteilung, die durch die Abarbeitung des parallelen Streams erfolgt. Dadurch werden unterschiedliche Teilkomponenten konkateniert, die Summe jedoch ist konstant, da der Wertevorrat gleich bleibt, genauso wie die Reihenfolge mit der die Daten in den Stream gelangen. A und B bilden das erste Wertepaar das ausgewertet wird, C und D ist das zweite Paar, dann folgt E. Die Teilergebnisse werden zusammengeführt. Deswegen sind an B die zwei Unterstriche und an E vier Unterstriche. Sortieren wir die Teilschritte in eine lesbarere Folge und geben immer v1, v2 und das Teilergebnis aus, so erhalten wir Listing 5.

v1 X_ plus v2_ B_ => X_B_
    v1 X_ plus v2_ E_ => X_E_
    v1 X_ plus v2_ D_ => X_D_
    v1 X_ plus v2_ C_ => X_C_
    v1 X_ plus v2_ A_ => X_A_
    v1 X_D_ plus v2_ X_E__ => X_D_X_E__
    v1 X_A_ plus v2_ X_B__ => X_A_X_B__
    v1 X_C_ plus v2_ X_D_X_E___ => X_C_X_D_X_E___
    v1 X_A_X_B__ plus v2_ X_C_X_D_X_E____ => X_A_X_B__X_C_X_D_X_E____

Fazit

Die Methode reduce ermöglich es, die Werte aus dem Quellstream miteinander zu verarbeiten, um ein einzelnes Ergebnis zu erhalten. Hierbei ist es wichtig die Unterscheidungen zwischen der seriellen und parallelen Verarbeitung zu beachten. Die Ergebnisse können unterschiedlich sein, was auf die jeweilige Reduktions-Transformation ankommt. Bei trivialen Dingen, wie das Auffinden eines Maximalwertes kommt es nicht zu Randerscheinungen. Jedoch sollte man durchaus auch bei vermeintlich trivialen Transformationen den Test machen, ob das Ergebnis immer noch dem gewünschten entspricht.
Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.