Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Effective Java

Effective Java
Java 8
reduce() vs. collect()
 

Java Magazin, November 2015
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 
 

Diesmal wollen wir uns die beiden Stream-Operationen  reduce() und  collect() genauer ansehen und sie dabei vergleichen.  Wir hatten beide Operationen jede für sich schon einmal in vorhergehenden Beiträgen besprochen;  reduce() in / KLSO / und  collect() in / KLSC /.

Java und Funktionale Programmierung

Mit dem Release 8 sind Elemente der Funktionalen Programmierung in Java dazugekommen.  Dies sind im Wesentlichen das Sprachmittel der Lambda Expressions (siehe / KLLE /) und die Erweiterung des JDK um Streams (siehe /KLSÜ/). Diese Erweiterungen machen aber aus Java keine funktionale Programmiersprache und das sollen sie auch gar nicht. Schließlich ist Java rund zwanzig Jahre alt und es wäre eher kontraproduktiv gewesen, mit der bisherigen objekt-orientierten Charakteristik von Java zu brechen.  Der Punkt war vielmehr, dass man die neuen  funktionalen Features so in Java einbringen wollte, dass sie möglichst nahtlos zum bisherigen Java passen.  Zur objekt-orientierten Programmierung in Java gehört die Verwendung von veränderlichem Zustand (Englisch: state   bzw. shared mutable state ).  Das ist ein Aspekt, der in der funktionalen Programmierung aber gar nicht unterstützt wird bzw. eher als falsches Konzept betrachtet wird. 

 
 

Das ist nun der Punkt, wo unser Paar  reduce() und  collect() ins Spiel kommen.   reduce() ist im Gegensatz zu  collect() eine klassische Rekuktionsoperation aus der funktionalen Programmierung.  Da es in funktionalen Sprachen keinen veränderlichen Zustand gibt, funktioniert  reduce() nur auf Typen mit unveränderlichem Zustand.  Da in Java aber Typen mit unveränderlichem Zustand eher untypisch sind, wollte man auch eine Reduktion auf veränderlichen Typen unterstützen.  Deshalb haben die JDK Entwickler mit  collect() eine Eigenentwicklung für Java hinzugefügt.  collect() ist die Reduktion auf veränderlichen Typen, während  reduce() die aus der funktionalen Welt stammende Reduktion auf unveränderlichen Typen ist.  Das Problem ist, dass  collect() Softwareentwicklern, die sich bereits mit funktionaler Programmierung auskennen, nicht bekannt ist und deshalb von ihnen meist ignoriert wird.

reduce() & collect()

Ein einfaches Beispiel mit  reduce()

Wie sieht das Ganze nun konkret aus?  Fangen wir mit einem Beispiel an, das es in ähnlicher Form in unserem Workshop gibt.  Die Aufgabe besteht darin, einen Stream der ganzen Zahlen von 0 bis 7 zu erzeugen, danach diese Zahlen auf Strings abzubilden und zuletzt diese Strings zu einem gesamten String zusammenzufassen, der dann  "01234567"   lautet.

 
 

Eine mögliche Lösung sieht so aus:

String s = IntStream. range (0, 8)
                    .mapToObj(Integer:: toString )
                    .reduce( "" , (s1, s2) -> s1 + s2);

System. out .println(s);
 
 

Diese Lösung verwendet das  reduce()  mit dem  String -Operator+ und dem leeren String als neutralem Anfangswert.  Diese Lösung produziert das richtige Ergebnis und sieht auch relativ knapp und elegant aus.  Leider ist die Performance aber relativ schlecht.  Denn mit jedem Ziffernstring, der ins  reduce() kommt, wird ein neuer  String erzeugt, der das aktuelle Teilergebnis repräsentiert.  Der  String -Operator+ erzeugt immer wieder einen neuen  String , weil  String ein unveränderlicher Typ ist.  Aber gerade weil  String ein unveränderlicher Typ ist, passt er zum  reduce() , so dass die Lösung funktioniert, wenn auch wie gesagt nicht besonders performant.

Ein einfaches Beispiel mit  collect()

Dass der  String -Operator+ nicht ideal ist für die Stringkonkatenation, lernt man im Allgemeinen schon im Java-Anfängerkurs.  Man lernt dort auch die performantere Alternative kennen: nämlich die  String s erst in einem  StringBuilder mit  append() akkumulieren und aus diesem am Ende mit  toString() den Ergebnisstring erzeugen. 

 
 

Wie kann man diesen Lösungsansatz auf unser Stream-Problem übertragen?  Das geht so:
 
 

String s = IntStream. range (0, 8)
                    .mapToObj(Integer:: toString )
                   .collect(Collectors. joining ());

System. out .println(s);
 
 

Statt  reduce() verwenden wir  collect() mit dem  Collector , der von der Factory-Methode  Collectors.joining() erzeugt wird.  Intern verwendet dieser  Collector den  StringBuilder mit  append() .
 
 

In den Workshops ist das Problem mit dieser Lösung meist, dass die Teilnehmer sie nicht besonders mögen, weil sie ziemlich intransparent ist.  Die Javadoc der  joining() -Factory-Methode sagt nur:  Returns a  Collector that concatenates the input elements into a  String , in encounter order. Dass die Lösung auf dem  StringBuilder basiert, findet man erst heraus, wenn man sich den Source Code im JDK ansieht.

Ein aufwändiges Beispiel mit  collect()

Entsprechend ist der Wunsch groß, das Ganze selbst zu implementieren, damit die unterliegenden Mechanismen ( StringBuilder mit  append() ) deutlicher sichtbar werden.  Eine Frage kommt an dieser Stelle häufig auf: Wenn man es selbst macht, geht es dann mit  collect() oder mit  reduce() ?

 
 

Die Antwort ist: man muss  collect () verwenden.   reduce() funktioniert nur für unveränderliche Typen wie   String .  Wir wollen aber eine Reduktion mit dem veränderlichen Typ  StringBuilder   machen.  Also müssen wir  collect () verwenden. 
 
 

In dem obigen Beispiel haben wir die Version des  collect() genutzt, die einen  Collector als Parameter nimmt:
 
 

R collect(Collector<? super T,A,R> collector)
 
 

Nun benötigen wir für die eigene Implementierung der Funktionalität mit Hilfe des  StringBuilder s die folgende, überladene Version des  collect() :
 
 

R               collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
 
 

Die Lösung damit sieht dann so aus:
 
 

String s = IntStream.range(0, 8)

                    .mapToObj(Integer::toString)

                    .collect(() -> new StringBuilder(),

                            (StringBuilder sb1, String s1) -> sb1.append(s1),

                             (StringBuilder sb1, StringBuilder sb2) -> sb1.append(sb2))

                    .toString();
 
 

  System.out.println(s);
 
 

Wenn man Methoden-Referenzen (statt Lambda Ausdrücke) verwendet, geht es auch noch etwas knapper:
 
 

String s = IntStream. range (0, 8)
                    .mapToObj(Integer:: toString )
                    .collect(StringBuilder::new,
                            StringBuilder::append,
                             StringBuilder::append)
                    .toString();

System. out .println(s);
 
 

Was haben wir jetzt eigentlich genau implementiert? Unser  collect() verlangt drei funktionale Parameter:  supplier accumulator und  combiner (siehe Signatur oben).  
 
 

Der  supplier beschreibt die Funktionalität, mit der das  collect() ein Zielobjekt erzeugt kann, in das dann im Weiteren die Elemente des Streams aufgesammelt werden.  Wir wollen unsere  String s in einem  StringBuilder aufsammeln, also ist die Implementierung des  suppliers () -> new StringBuilder() , oder als Methoden-Referenz einfach:  StringBuilder::new .
 
 

Der  accumulator beschreibt, wie das  collect() ein Element aus dem Stream (also einen  String ) in dem Zielobjekt (also dem  St r ingBuilder ) aufsammeln soll.  Das hatten wir uns oben schon überlegt.  Es soll die  append() -Methode des  StringBuilders mit dem  String als Parameter verwendet werden.  Das heißt, die Implementierung mit einem Lambda-Ausdruck ist:  (StringBuilder sb1, String s1) -> sb1.append(s1) , als Methoden-Referenz einfach:  StringBuilder::append
 
 

Der dritte Parameter (der  combiner ) wird nur bei einem parallelen Stream benutzt.  Trotzdem müssen wir ihn übergeben, da es nur ein API für parallele und sequentielle Streams gibt (siehe /KLPS/).  Der  combiner beschreibt, wie zwei Zielobjekte (also zwei  StringBuilder ) zusammen-kombiniert werden.  Das ist relativ einfach und zwar, indem man den zweiten  StringBuilder mit  append() an den ersten anhängt.  Als Lambda Ausdruck ist dies:  (StringBuilder sb1, StringBuilder sb2) -> sb1.append(sb2) und als Methoden-Referenz einfach wieder:  StingBuilder::append
 
 

Beim parallelen Stream (siehe / KLPS /) werden in der Ausführungsphase mit dem  supplier mehrere  StringBuilder parallel erzeugt und befüllt ( a ccumulator ).  Der  combin er wird dann benutzt, um in der Join-Phase die verschiedenen  StringBuilder zu einem Gesamtergebnis zusammenzufassen.
 
 

Damit haben wir uns im Detail angesehen, wie man mit Hilfe des  collect() die Reduktion auf Basis des  StringBuilder s selbst implementieren kann.  

Ein falsches Beispiel mit  reduce()

Wie sieht es nun aus, wenn man die Reduktion auf Basis des  StringBuilders mit  reduce() versucht.  Wie und wo scheitert man da?  Findige Workshopteilnehmer unternehmen gerne den Versuch mit einer Version des  reduce() , die der oben verwendeten Version von  collect() sehr ähnlich sieht:

 
 

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

Auch wenn hier die Typen von  accumulator ( BiFunct ion statt  BiConsumer ) und  combiner ( BinaryOperator statt  BiConsumer ) andere sind als beim  collect() , so kann man sie beide wieder mit einer Methoden-Referenz  StringBuilder::append implementieren:
 
 

// Falsch ! Falsch! Falsch! Falsch! Falsch!

String s = IntStream. range (0, 8)
                    .mapToObj(Integer:: toString )
                    .reduce( new StringBuilder(), StringBuilder::append, StringBuilder::append)
                    .toString();

System. out .println(s);
 
 

Lässt man den Code laufen, wird sogar der gewünschte Ergebnis- String "01234567" erzeugt. Trotzdem ist die Implementierung mit  reduce() falsch und die mit  collect() richtig.  Testen kann man es am einfachsten dadurch, dass man einen parallelen Stream statt eines sequentiellen verwendet, also den Code folgendermaßen ändert:
 
 

// Falsch ! Falsch! Falsch! Falsch! Falsch!

String s = IntStream. range (0, 8) . parallel()
                    .mapToObj(Integer:: toString )
                    .reduce( new StringBuilder(), StringBuilder::append, StringBuilder::append)
                    .toString();

System. out .println(s);
 
 

Der Ergebnis- String ist dann auf unserer Dual-Core-Plattform:
 
 

" 456745672301456745672301456745672301456745672301 "  
 
 

Der falsche Ergebnis- String ist plattform-abhängig, im Wesentlichen abhängig von der Zahl der CPU-Cores und thread-safe ist es auch nicht, weil der  StringBuilder nicht thread-safe ist.

Der Algorithmus von  reduce() läuft nämlich so, dass der eine  StringBuilder , der dem  reduce() übergeben wird, in der Execution-Phase von allen parallelen Worker-Threads gleichzeitig verwendet wird.  Jeder Thread hängt die Elemente aus seinem  Spli terator der Stream-Source an diesen einen  StringBuilder an.  Damit das verlässlich funktioniert, müsste der  StringBuilder thread-safe sein; das ist er aber nicht.  In der anschließenden Join-Phase werden die Teilergebnisse zum Gesamtergebnis zusammengefasst.  Beim  reduce() sind aber gar keine Teilergebnisse entstanden, sondern alle Worker-Threads haben wahllos einen einzige  StringBuilder Instanz modifiziert.  Diese eine  StringBuilder Instanz wird dann als Teilergebnis des jeweiligen Worker-Threads angesehen und in der Join-Phase x-mal an sich selbst angehängt.  Das Ergebnis ist völlig falsch, weil  reduce() davon ausgeht, dass die Typen unveränderlich sind.  Es wird erwartet, dass der  accumulator und der  combiner in jedem Schritt ein neues unveränderliches Objekt erzeugen.  Stattdessen haben wir ein einziges veränderliches Objekt modifiziert und genau das ist falsch.
 
 

Beim  collect() oben ist es anders.  Dem  collect() wird kein  StringBuilder , sondern ein Supplier für einen  StringBuilder übergeben und jeder Worker-Thread erzeugt sich mit dem Supplier seinen eigene  StringBuilder Instanz, die er ganz alleine verwendet und ganz alleine modifiziert. Beim  collect() wird - anders als beim  reduce() - keine Thread-Safety gebraucht und es ist ausdrücklich erlaubt, dass Objekte verändert werden. Anschließend in der Join-Phase werden die einzelnen  StringBuilder , die in der Execution-Phase entstanden sind, aneinandergehängt, um aus den Teilergebnissen das Gesamtergebnis zu erzeugen.

Fazit

Wir haben uns damit umfassend angesehen:

reduce() ist für eine Reduktion auf Basis von unveränderliche Typen (zum Beispiel:  String ),

collect() ist für eine Reduktion auf Basis von veränderlichen Typen (zum Beispiel:  StringBuilder ).
 
 

Wenn man die Javadoc der Methoden sehr aufmerksam vergleicht, kann man das auch dort herauslesen.  Die Javdoc von  reduce() beginnt mit: “ Performs a reduction on the elements of this stream …” , und die von  collect() beginnt mit:  “ Performs a mutable reduction operation on the elements of this stream … ” .  Zugegeben, die Unterschiede in der Beschreibung sind recht subtil.
 
 

Weiter hilft es auch, die Signaturen von  accumulator und  combiner beim  reduce() und  collect() zu vergleichen.  Beim  reduce() sind  accumulator und  combiner vom Typ  BiFunction und  BinaryOperator , d.h. sie liefern ein Ergebnis, weil der unterliegende Typ unveränderlich ist.  Beim  collect() sind  accumulator und  combiner beide vom Typ  BiConsumer und produzieren kein Ergebnis, sondern verändern stattdessen jeweils den Input-Parameter, der das Zielobjekt repräsentiert.  Auch hier ist der Unterschied recht subtil.
 
 

Obwohl  reduce() die klassische Methode aus der funktionalen Programmierung ist, spielt sie bei den Java Streams im Vergleich zu  collect() nicht unbedingt die wichtigere Rolle.  Das liegt daran, dass in Java die veränderliche Reduktion oft performanter und deshalb vorzuziehen ist.  Das haben wir ja an unserem Beispiel mit der  String -Konkatenation bereits ausführlich diskutiert. 

Horror Code

Genaugenommen ist die Regel " reduce() für unveränderliche Typen" und " collect() für veränderliche Typen" etwas verkürzt.  Es kommt nämlich nicht allein auf den Typ an, den man in der Reduktion verwendet, sondern auch darauf, ob man die Reduktion unverändernd oder verändernd implementiert.

 
 

Schauen wir uns dazu ein Beispiel an, bei dem wir auf Basis von  reduce() eine unveränderliche Reduktion mit  StringBuilder implementieren.  Das geht so:
 
 

String s = IntStream.range(0, 8).parallel()

                    .mapToObj(Integer::toString)

                    .reduce(new StringBuilder(),

                             (sb1, s1) -> new StringBuilder(sb1).append(s1),

                             (sb1, sb2) -> new StringBuilder(sb1).append(sb2))

                    .toString();
 
 

  System.out.println(s);
 
 

Wir implementieren sowohl den  accumulator als auch den  combiner so, dass sie nicht das vorhandene  StringBuilder -Objekt  sb1 verändern, sondern mit  new ein neues  StringBuilder Objekt erzeugen, an das der  String (beim  accumulator ) bzw. der  StringBuilder (beim  combiner ) angehängt wird.  Der Code funktioniert korrekt auch bei einem parallelen Stream.  Das Problem ist aber natürlich - wie auch beim  reduce() mit  String - die Performance, da in jedem Reduktionsschritt ein neues Objekt erzeugt wird.  Der Performance-Vorteil, den man erzielen könnte, weil  Strin gBuilder ein veränderlicher Typ ist, wird bei dieser Lösung nicht genutzt.  Das Beispiel zeigt nur, dass auch die Kombination  reduce() und  StringBuilder möglich ist.  Besonders sinnvoll ist sie nicht.  Nicht umsonst ist der Titel dieses Absatzes: Horror Code .
 
 

Der Vollständigkeit halber: Gibt es auch die Möglichkeit,  collect() und  String zu kombinieren?  Ja, auch das geht irgendwie.  Da  String als unveränderlicher Typ implementiert ist, geht es mit  String direkt nicht.  Aber wir können ein  String -Array der Länge 1 als veränderliches Zielobjekt des  collect() verwenden.  Die Implementierung sieht dann so aus:
 
 

String s = IntStream.range(0, 8).parallel()

                    .mapToObj(Integer::toString)

                    .collect(() -> { String[] sa = {""};

                                     return sa;

                                   }, (sa1,s1) -> sa1[0]+=s1, (sa1,sa2) -> sa1[0]+=sa2[0])[0];

System.out.println(s);
 
 

Bezüglich der Performance hilft uns diese Lösung aber auch nicht, da von der nullten Stelle des Arrays nach jedem Reduktionsschritt ein neues  String -Objekt referenziert wird.  Der Grund dafür ist der in  accumulator und  combiner verwendete Operator+= von  String .  Also auch hier wieder: Horror Code
 
 

Den Horror Code bitte nicht nachmachen.  Wir wollten nur illustrieren, dass man mit  reduce() und  collect() jede Menge Unfug machen kann.  Wichtig für die Praxis ist, dass man sich anschaut, ob man eine Reduktion mit einem veränderlichen oder unveränderlichen Typ machen will, und dann gilt die Regel: " reduce() für unveränderliche Typen" und " collect() für veränderliche Typen".  Alles andere ist entweder performance-mäßig indiskutabel oder schlicht falsch.

Zusammenfassung und Vorschau

Wir haben uns diesmal angesehen, wie sich die Stream-Operationen  reduce() und  collect() unterscheiden bzw. ergänzen.  Beim nächsten Mal wollen wir uns ansehen wie mächtig  collect() sein kann, wenn man benutzerdefinierte Typen in der veränderlichen Reduktion verwendet.

Literaturverweise

/KLLE/   Lambda-Ausdrücke und Methoden-Referenzen
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html
/KLPS/       Parallel Streams
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/81.Java8.Parallel-Streams/81.Java8.Parallel-Streams.html
/KLSC/   Stream-Kollektoren und die Stream-Operation collect()
Klaus Kreft & Angelika Langer, Java Magazin, September 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/76.Java8.Stream-Collectors/76.Java8.Stream-Collectors.html
/KLSO/ Stream-Erzeugung und Stream-Operationen
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/75.Java8.Fundamental-Stream-Operations/75.Java8.Fundamental-Stream-Operations.html
/KLSU/ Ü bersicht über das Stream API in Java 8
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/74.Java8.Streams-Overview/74.Java8.Streams-Overview.html

Die gesamte  Serie über Java 8:

/JAV8-0/ Neue Features in Java 8 - Überblick
Klaus Kreft & Angelika Langer, Java Magazin, März 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/73.Java8.Overview/73.Java8.Overview.html
/JAV8-1/ Funktionale Programmierung in Java
Klaus Kreft & Angelika Langer, Java Magazin, September 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/70.Java8.FunctionalProg/70.Java8.FunctionalProg.html
/JAV8-2/ Lambda-Ausdrücke und Methoden-Referenzen
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html
/JAV8-3/ Default-Methoden und statische Methoden in Interfaces
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/72.Java8.DefaultMethods/72.Java8.DefaultMethods.html
/JAV8-4/ Übersicht über das Stream API in Java 8
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/74.Java8.Streams-Overview/74.Java8.Streams-Overview.html
/JAV8-5/ Stream-Erzeugung und Stream-Operationen
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/75.Java8.Fundamental-Stream-Operations/75.Java8.Fundamental-Stream-Operations.html
/JAV8-6/ Stream-Kollektoren und die Stream-Operation collect()
Klaus Kreft & Angelika Langer, Java Magazin, September 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/76.Java8.Stream-Collectors/76.Java8.Stream-Collectors.html
/JAV8-7/ Stateful Lambdas - Regeln für die Seiteneffekte in Lambda-Ausdrücken, die an Stream-Operationen übergeben werden
Klaus Kreft & Angelika Langer, Java Magazin, November 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/77.Java8.Streams-and-Statefulness/77.Java8.Streams-and-Statefulness.html
/JAV8-8/ Das Date/Time API
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/78.Java8.Date-Time-API/78.Java8.Date-Time-API.html
/JAV8-9/ CompletableFuture
Klaus Kreft & Angelika Langer, Java Magazin, März 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/79.Java8.CompletableFuture/79.Java8.CompletableFuture.html
/JAV8-10/ Optional<T>
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/80.Java8.Optional-Result/80.Java8.Optional-Result.html
/JAV8-11/ Parallel Streams
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/81.Java8.Parallel-Streams/81.Java8.Parallel-Streams.html
/JAV8-12/ Das Performance-Modell der Streams
Klaus Kreft & Angelika Langer, Java Magazin, September 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/82.Java8.Performance-Model-of-Streams/82.Java8.Performance-Model-of-Streams.html
/JAV8-13/ reduce() vs. collect()
Klaus Kreft & Angelika Langer, Java Magazin, November 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/83.Java8.Reduce-vs-Collect-Stream-Operations/83.Java8.Reduce-vs-Collect-Stream-Operations.html
/JAV8-14/ User-Defined Collectors
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/84.Java8.User-Defined-Stream-Collectors/84.Java8.User-Defined-Stream-Collectors.html
/JAV8-15/ Parallele Streams und Blockierende Funktionalität
Klaus Kreft & Angelika Langer, Java Magazin, März 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/85.Java8.Streams-and-Blocking-Functionality/85.Java8.Streams-and-Blocking-Functionality.html
/JAV8-16/ API-Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/86.Java8.API-Design-With-Lambdas/86.Java8.API-Design-With-Lambdas.html
/JAV8-17/ Low-Level-Aspekte beim API Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/87.Java8.Programming-With-Lambdas/87.Java8.Programming-With-Lambdas.html
/JAV8-18/ Benutzer-definierte Stream-Sourcen und Spliteratoren
Klaus Kreft & Angelika Langer, Java Magazin, September 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/88.Java8.User-Defined-Stream-Sources-And-Spliterators/88.Java8.User-Defined-Stream-Sources-And-Spliterators.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
Lambdas & Streams - Java 8 Language Features and Stream API & Internals
3 day seminar ( open enrollment and on-site)
Java 8 - Lambdas & Stream, New Concurrency Utilities, Date/Time API
4 day seminar ( open enrollment and on-site)
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
Related Reading
Lambda & Streams Tutorial & Reference
In-Depth Coverage of all aspects of lambdas & streams
Lambdas in Java 8
Conference Presentation at JFokus 2012 (slides)
Lambdas in Java 8
Conference Presentation at JavaZone 2012 (video)
 

 
 
  © Copyright 1995-2018 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/83.Java8.Reduce-vs-Collect-Stream-Operations/83.Java8.Reduce-vs-Collect-Stream-Operations.html  last update: 26 Oct 2018