https://delors.github.io/prog-adv-java-streams/folien.de.rst.html
https://delors.github.io/prog-adv-java-streams/folien.de.rst.html.pdf
Die Folien wurden insbesondere basierend auf der offiziellen Java Dokumentation (JavaDoc 24 und JEPs), sowie basierend auf der Scala (2.13 bzw. 3) Documentation erstellt. Die Folien bzgl. der konkreten Java Streams API (Deepdive) basieren auf Folien von Th. Letschert
Bei der Erstellung der Folien wurden KI Assistenten unterstützend eingesetzt. Dies erfolgte insbesondere, um effizient Grafiken zu generieren, die sich leicht in die Folien integrieren lassen, oder um sich Übersichtstabellen generieren zu lassen. Weiterhin wurde KI zur allgemeinen Qualitätssicherung eingesetzt. Alle Inhalte, die in diesem Rahmen von der KI (zusäztlich) vorgeschlagen wurden, wurden überprüft.
Es wurde keine KI verwendet für den Aufbau, die Struktur und insbesondere die Auswahl der grundlegenden Inhalte.
Konzept
Beschreibung
Beispiel
Lambda-Ausdruck
Eine anonyme Funktion, die als Wert übergeben werden kann. Syntax:
(Parameter) -> Ausdruck
(Parameter) -> { Anweisungen; }
(x, y) -> x + y
x -> x * x
() -> 42
(a, b) -> { int s = a + b; return s * 2; }Methoden-Referenz
Kurzschreibweise für Lambdas, die lediglich eine bestehende Methode aufrufen. Varianten:
statisch:
Klasse::methodeauf Instanz:
objekt::methodeauf Typ:
Klasse::instanzMethodeKonstruktor:
Klasse::new
Integer::parseInt
System.out::println
String::length
ArrayList::newFunktionales Interface
Ein Interface mit genau einer abstrakten Methode (SAM — Single Abstract Method). Kann mit
@FunctionalInterfaceannotiert werden und ermöglicht die Verwendung von Lambda-Ausdrücken.@FunctionalInterface interface MyFunc { int apply(int a, int b); }
Predicate<T>Funktionales Interface: Prüft eine Bedingung auf einem Wert und gibt
booleanzurück.
Predicate<Integer> isEven = x -> x % 2 == 0;
Function<T,R>Funktionales Interface: Bildet einen Wert vom Typ
Tauf einen Wert vom TypRab.
Function<String,Integer> len = String::length;
Consumer<T>Funktionales Interface: Konsumiert einen Wert, gibt nichts zurück (
void).
Consumer<String> printer = System.out::println;
Supplier<T>Funktionales Interface: Liefert einen Wert ohne Eingabe.
Supplier<Double> random = Math::random;
BinaryOperator<T>Funktionales Interface: Verknüpft zwei Werte desselben Typs zu einem Ergebnis.
BinaryOperator<String> wrap = (a, b) -> b + a + b;
Comparator<T>Funktionales Interface für den Vergleich von zwei Objekten; gibt einen
int-Wert zurück.
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Comparator.comparing(String::length)Funktionen höherer Ordnung
Funktionen, die andere Funktionen als Parameter nehmen oder als Ergebnis zurückgeben. Grundlage aller Stream-Operationen.
stream.filter(x -> x > 0).map(x -> x * 2)Closure
Ein Lambda-Ausdruck, der auf Variablen aus dem umgebenden Kontext zugreift. Diese Variablen müssen
finaloder effektiv final sein.final int factor = 10; Stream.of(3,4,3). map(x -> x * factor). toList();
Optional<T>Ein Container, der einen Wert enthalten kann oder leer ist. Ersetzt die Verwendung von
null.
Optional.of(42)
Optional.empty()
Bei der Datenverarbeitung von:
großen Datenmengen,
die nicht (ohne weiteres) in den Speicher passen oder
bei denen es keinen Sinn macht diese vorab vollständig in den Speicher zu laden, da immer nur ein kleiner Abschnitt analysiert werden muss
Daten, die kontinuierlich verarbeitet werden müssen
Parallele Verarbeitung und effiziente Nutzung von modernen CPU-Architekturen
Insbesondere die korrekte und effiziente Nutzung mehrere Threads ist häufig schwierig. Die Verwendung von (soweit möglich) „transparent“ parallelisierten Streams ist dabei sehr hilfreich.
(Z. B. mit Java Parallel Streams oder .par in Scala ist eine weitgehend transparente Parallelisierung möglich.)
Entwicklung von Software nahe am Domänenmodell, um die korrekte Implementierung zu unterstützen.
Das Vorgehen wird detailliert durch konkrete Anweisungen beschrieben, die genau vorgeben welche einzelnen Schritte von dem Computer ausgeführt werden sollen, um das Ziel zu erreichen.
Viele gängige general-purpose Programmiersprachen (C, C++, Rust, Java, Go, JavaScript, Python) sind im Kern imperative Programmiersprachen.
Das Ziel ist es auszudrücken was erreicht werden soll, ohne das wie genau anzugeben.
(Prominentes Beispiel für eine deklarative Programmiersprache: SQL als Datenbankabfragesprache.)
Streams[1] sind umgeformte Sammlungen, die durch die Umformung für funktional-orientierte Massen-Operationen geeignet sind.
Streams erlauben die „korrekte, effiziente, lesbare (domänennahe)“ Verarbeitung von Daten mit Hilfe von Konzepten und Ideen aus der funktionalen und deklarativen Programmierung.
Es ist möglich als terminale Operation einen Iterator bzw. Spliterator zu erzeugen. In diesen beiden Fällen erfolgt die Evaluation der Pipeline nicht vollständig (d.h. nicht eager sondern lazy). Es wird somit nur so viel ausgewertet, wie für die Erzeugung des Iterator/Spliterator notwendig ist. Das hat zur Folge, dass die Elemente des Streams erst dann verarbeitet werden, wenn sie tatsächlich über den Iterator/Spliterator angefordert werden.
Die Verwendung dieser beiden Methoden führt zu einem Bruch des deklarativen Pipeline-Modells, der in den allermeisten Fällen vermieden werden kann. Häufig nur noch im Zusammenhang mit Legacy-APIs relevant, die mit Iterator/Spliterator arbeiten.
Transformieren die Elemente jeweils völlig unabhängig von allen anderen.
Transformieren die Elemente abhängig von anderen.
Operation |
Stateless / Stateful |
Speicherbedarf |
Parallelisierbar |
Short-circuiting |
|---|---|---|---|---|
filter |
Stateless |
Konstant |
Trivial |
Nein |
map |
Stateless |
Konstant |
Trivial |
Nein |
flatMap |
Stateless |
Konstant |
Trivial |
Nein |
mapMulti |
Stateless |
Konstant |
Trivial |
Nein |
peek |
Stateless |
Konstant |
Trivial |
Nein |
sorted |
Stateful |
O(n) |
Eingeschränkt |
Nein |
distinct |
Stateful |
O(n) |
Eingeschränkt |
Nein |
limit |
Stateful |
Konstant |
Eingeschränkt |
Ja |
skip |
Stateful |
Konstant |
Eingeschränkt |
Nein |
takeWhile |
Stateful |
Konstant |
Eingeschränkt |
Ja |
dropWhile |
Stateful |
Konstant |
Eingeschränkt |
Nein |
gather |
Konfigurierbar |
Konfigurierbar |
Konfigurierbar |
Konfigurierbar |
Warnung
Seiteneffekte in Funktionen, die an Stream-Operationen übergeben werden, sind grundsätzlich zu vermeiden. Wird ein Stream parallelisiert und eine übergebene Funktion (z. B. an die peek Methode) hat dennoch Seiteneffekte, so muss diese Thread-sicher sein.
Stream<T> ist der Typ der Streams mit Objekten vom Typ T
Streams mit primitiven Daten:
IntStream
LongStream
DoubleStream
Diese Streams mit primitiven Daten arbeiten in vielen Fällen effizienter jedoch sind manche Operationen nur auf Object-Streams erlaubt. „Primitive“ Streams können mit der Methode boxed in Object-Streams umgewandelt werden.
Beispiel
1IntStream isPrim = IntStream.range(1, 10);2Stream<Integer> isObj = isPrim.boxed();
Statische Methoden in Arrays
Die Klasse java.util.Arrays hat mehrere überladene statische stream-Methoden, mit denen Arrays in Ströme umgewandelt werden können.
Die Streams können Objekte oder primitive Daten enthalten.
Beispiel
1// Stream of primitive data:2IntStream isP = Arrays.stream(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 });3// Stream of objects:4Stream<Integer> isO = Arrays.stream(5new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }6);
Statische Methoden in Stream
Das Interface java.util.stream.Stream enthält mehrere statische Methoden mit denen Streams erzeugt werden können.
Für die Klassen der Streams mit primitiven Werten (z.B. java.util.stream.IntStream) gibt es äquivalente Methoden.
Mit of werden die übergebenen Wert in einen Stream gepackt.
Mit iterate und generate hat man eine einfache Möglichkeit unendliche Ströme zu erzeugen.
Beispiel
1// Object-Stream 1, 2 ... 9, 0:2Stream<Integer> is1a = Stream.of(1,2,3,4,5,6,7,8,9,0);
2// int-Stream 1, 2, ... 9, 03IntStream is1b = IntStream.of(1,2,3,4,5,6,7,8,9,0);
3// (infinite) Stream 1, 2, ...4Stream<Integer> is2 = Stream.iterate(1, ((x) -> x+1));
4int[] z = new int[]{1};5Stream<Integer> is3 = Stream.generate((() -> z[0]++)); // (infinite) Stream 1, 2, ...
Statische range-Methoden in IntStream und LongStream
Die Interfaces java.util.stream.IntStream und java.util.stream.LongStream enthalten jeweils zwei statische range-Methoden mit denen Streams erzeugt werden können.
Beispiel
1IntStream isPrimA = IntStream.range(1, 10); // 1,2, .. 92IntStream isPrimA = IntStream.rangeClosed(1, 10); // 1,2, .. 9, 10
Nicht-statische Methoden der Collection-API
Das Interface java.util.Collection enthält die Methode stream mit der die jeweilige Kollektion in einen Stream umgewandelt werden kann.
Beispiel
1 Stream<Integer> is = Arrays.asList(1,2,3,4,5,6,7,8,9,0).stream();
Zustandslose Verarbeitungsoperationen
map(Function<? super T,? extends R> mapper): Transformiert jedes Element in ein anderes.
filter(Predicate<? super T> predicate): Filtert Elemente heraus.
flatMap(Function<? super T,? extends Stream<? extends R>> mapper): Transformiert jedes Element in einen Stream und fügt die Streams zusammen.
Beispiel
1import java.util.List;2import java.util.stream.Collectors;3import java.util.stream.IntStream;45List<Integer> is = IntStream.range(1, 10)6.filter(i -> i % 2 != 0)7.peek(i -> System.out.print(i+ " "))8.map(i -> 10 * i)9.boxed()10.collect(Collectors.toList());11System.out.println(is);
Ausgabe: 1 3 5 7 9 [10, 30, 50, 70, 90]
Beispiel
1import java.util.List;2import java.util.stream.Collectors;3import java.util.stream.IntStream;4import java.util.stream.Stream;56static Stream<Integer> range(int from, int to) {7return IntStream.range(from, to).boxed();8}910List<Integer> is = Stream.of(0, 1, 2)11.flatMap(i -> range(10 * i, 10 * i + 10))12.collect(Collectors.toList());
Ausgabe: is ==> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
Zustandsbehaftete Verarbeitungsoperationen
distinct(): Entfernt Duplikate.
sorted(): Sortiert die Elemente.
sorted(Comparator<? super T> comparator): Sortiert die Elemente mit einem gegebenen Comparator.
limit(long maxSize): Begrenzt die Anzahl der Elemente.
skip(long n): Überspringt die ersten n Elemente.
Beispiel
1import java.util.List;2import java.util.stream.Collectors;3import java.util.stream.Stream;45List<Integer> lst = Stream.of(9, 0, 3, 1, 7, 3, 4, 7, 2, 8, 5, 0, 6, 2)6.distinct()7.sorted((i, j) -> i - j)8.skip(1)9.limit(3)10.collect(Collectors.toList());
Ausgabe: is ==> [1, 2, 3]
Verarbeitungsoperationen
Eine terminale Operation hat im Gegensatz zu den Verarbeitungsoperationen keinen Stream als Ergebnis.
Terminale Operationen ohne Ergebnis
forEach(Consumer<? super T> action)
Wendet die übergebene Aktion auf alle Elemente des Streams an.
Terminale Operationen mit Ergebnis
Operationen mit Array-Ergebnis: Stream => Array
Operationen die den Stream in ein äquivalentes Array umwandeln.
Operationen mit Kollektions-Ergebnis: Stream => Kollektion
Operationen die den Stream in eine äquivalente Kollektion umwandeln.
Operationen mit Einzel-Ergebnis: Aggregierende Operationen
Operationen die den Stream zu einem einzigen Wert verarbeiten.
Beispiel
forEach
1Stream.of(9, 0, 3, 1, 7, 3, 4, 7, 2, 8, 5, 0, 6, 2)2.distinct()3.sorted( (i,j) -> i-j )4.limit(3)5.forEach( System.out::println );
Beispiel
toArray
1int[] a = IntStream.range(1, 3).toArray();23Object[] a = Stream.of("1", "2", "3").map( Integer::parseInt )4.toArray();56Integer[] a = (Integer[]) Stream.of(1, 2, 3)7.toArray();89String[] a = Stream.of(1, 2, 3).map( (i) -> i.toString() )10.toArray( String[]::new ); // using generator
Terminale Operationen mit Kollektions-Ergebnis
Die Methode collect erzeugt eine Kollektion aus den Elementen des Streams.
IntStream und andere Streams mit primitiven Daten haben keine entsprechende Operation.
Das Argument von collect ist ein java.util.stream.Collector. Die Erzeugung einer Kollektion ist damit Sonderfall einer aggregierenden Operation.
Für die Erzeugung einer Kollektion verwendet man typischerweise einen vordefinierten Collector aus der Klasse java.util.stream.Collectors.
Einfache Kollektionserzeuger in Collectors sind:
toList()
toSet()
toCollection(Supplier<C> collectionFactory)
Beispiel
collect
1List<Integer> l1 = Stream.of(1, 2, 3).collect( Collectors.toList() );23List<Integer> l2 = IntStream.range(1, 4).boxed()4.collect( Collectors.toList() );56Set<String> s1 = (Set<String>) Stream.of("1", "2", "3")7.collect( Collectors.toSet());89Set<String> s2 = (Set<String>) Stream.of("1", "2", "3")10.collect( Collectors.toCollection( HashSet::new) );
1// Generating a map from a stream of strings23Map<String, Integer> m = Stream.of("1", "2", "3")4.collect(5Collectors.toMap(6(s) -> s,7Integer::parseInt8)9);
In Collectors finden sich Kollektoren mit denen Maps erzeugt werden können, die eine Gruppierung bzw. eine Partitionierung der Stream-Elemente darstellen:
static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
Gruppiert die Elemente entsprechend einer Klassifizierungsfunktion.
static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)
Partitioniert die Elemente entsprechend einem Prädikat.
Beispiel
collect(groupingBy)
Hilfreiche Methoden
1import static java.util.stream.Collectors.groupingBy;2import static java.util.stream.Collectors.partitioningBy;3import static java.util.stream.Collectors.counting;
1Map<Integer, List<Integer>> groupedByMod3 = Stream.of(1, 2, 3, 4, 5, 6 ,7 ,8, 9)2.collect( groupingBy( (x) -> x%3 ) );
Ausgabe: groupedByMod3 = {0=[3, 6, 9], 1=[1, 4, 7], 2=[2, 5,8]}
1Map<Integer, List<String>> groupedByLength = Stream.of(2"one", "two", "three", "four", "five", "six", "seven", "eight")3.collect( groupingBy( (s) -> s.length() ) );
Ausgabe: groupedByLength ==> {3=[one, two, six], 4=[four, five], 5=[three, seven, eight]}
Das Interface Stream bzw. die Interfaces für Ströme primitiver Daten (IntStream, etc.) bieten einige einfache aggregierende Funktionen für Standardoperationen auf allen Elementen des Stroms.
Beispiel
1long count = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).count();23long sum = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).sum();45OptionalDouble av = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).average();
Das Interface Stream bieten einige einfache aggregierende Funktionen für den Test aller Elemente des Stroms mit einem übergebenen Prädikat.
Beispiel
1boolean anyEven = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)2.anyMatch( (x) -> x%2 == 0 );34boolean allEven = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)5.allMatch( (x) -> x%2 == 0 );67boolean noneEven = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)8.noneMatch( (x) -> x%2 == 0 );
Das Interface Stream bietet die Funktionen findFirst und findAny für die „Suche“ nach dem ersten bzw. irgendeinem Element in einem Stream.
Achtung!
Diese Methoden haben kein Prädikat als Parameter. Es empfiehlt sich darum den Stream vorher mit dem entsprechenden Prädikat zu filtern.
Beispiel
1Optional<Integer> firstEven = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)2.filter( (x) -> x%2 == 0 )3.findFirst();
Ausgabe: firstEven ==> Optional[2]
Das Interface Stream bietet die Funktion
Optional<T> reduce(BinaryOperator<T> accumulator)
mit der eine Funktion auf jedes Element und das bisherige Zwischenergebnis angewendet werden kann.
Falls der erste Wert nicht der Startwert sein soll, verwendet man:
Optional<T> reduce(T identity, BinaryOperator<T> accumulator)
Beispiel
reduce
1 Optional<Integer> sumOfAll = Stream.of(1, 2, 3, 4, 5).reduce( (a, x) -> a+x );
Ausgabe: sumOfAll ==> Optional[15]
1 Optional<Integer> subOfAll = Stream.of(1, 2, 3, 4, 5).reduce( (a, x) -> a-x );
Ausgabe: subOfAll ==> Optional[-13]
1int sumOfAllPlus100 = Stream.of(1, 2, 3, 4, 5)2.reduce(100, (a, x) -> a+x );
Ausgabe: sumOfAllPlus100 ==> 115
Es gibt einen Kollektor mit dem String-Elemente zu einem String konkateniert werden können:
static Collector<CharSequence,?,String> joining(CharSequence delimiter)
Beispiel
reduce
1String concat = Stream.of("one", "two", "three")2.collect( joining("+") );
Ausgabe: concat = one+two+three
Ausgewählte Eigenschaften des Basisinterface aller Streams
Parallele und sequentielle Streams.
1package java.util.stream;23public interface BaseStream<T, S extends BaseStream<T,S>> {4/** Closes the stream, releasing any resources associated with it. */5void close();67/** Returns an equivalent stream that is parallel. */8S parallel();9/** Returns an equivalent stream that is sequential. */10S sequential();11}
13public interface Stream<T> extends BaseStream<T, Stream<T>> {14// ...15}
Die Interfacedefintion (BaseStream<T, S extends BaseStream<T,S>>) ist eine Anwendung des CRTP; d. h. des Curiously Recurring Template Patterns. Bei diesem Idiom haben wir eine Klasse X, die von einer generischen Klasse oder einem generischen Interface S abgeleitet wird, wobei die ableitende Klasse X sich selber als Typparameter verwendet. Dies erlaubt die Definition einer Fluent-API, bei der Methoden, die in der Basisklasse definiert sind, den abgeleiteten Typ zurückgeben.
Erzeugen von eigenen Streams mittels StreamSupport
Die Implementierung des Interfaces Stream<T> ist ggf. sehr aufwändig. Alternativ kann die Klasse StreamSupport verwendet werden, um auf einem Spliterator basierende Streams zu erzeugen.
1package java.util.stream;23public final class StreamSupport {45/** Creates a new sequential or parallel Stream from a Spliterator. */6static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel);78// ...9}
Streams und Lambda Ausdrücke
1void main() {2List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana", "Eve");3List<Integer> grades = Arrays.asList(85, 42, 91, 67, 55);45// TODO 1: Using a lambda, filter the grades list to only keep grades >= 606// and print the passing grades.7// Hint: Use stream(), filter(), and forEach().8910// TODO 2: Create a Predicate<Integer> lambda called `isExcellent`11// that returns true if a grade is 90 or above. Then print all excellent grades.121314// TODO 3: Create a Function<Integer, String> lambda called `toLetterGrade`15// that converts a numeric grade to a letter:16// 90+ → "A"; 75+ → "B" ; 60+ → "C"; below 60 → "F"17// Then print each grade alongside its letter grade.181920// TODO 4: Using a lambda, calculate and print the average of all grades.21// Hint: Use stream() and mapToInt().22}
Java Streams
Bemerkung
Verwenden Sie ausschließlich Streams und Lambda-Ausdrücke.
Schreiben Sie eine Methode int sumOfSquares(int[] a) die die Elemente des Arrays quadriert und dann die Summe berechnet.
Schreiben Sie eine Methode int sumOfSquaresEven(int[] a) die die Elemente des Arrays quadriert, und dann die Summe berechnet für alle Elemente die gerade sind.
Schreiben Sie eine Methode, die eine Liste von Strings (List<String>) in eine flache Liste von Zeichen (List<Integer>) umwandelt.
Schreiben Sie eine Methode, die die Zahlen von 1 bis Integer.MAX_VALUE addiert. Nutzen Sie IntStream.range() um die Zahlen zu iterieren. Messen Sie die Ausführungsdauer für die sequentielle und parallele Ausführung (siehe Anhang für eine entsprechende Methode zur Zeitmessung.)
Um die Ausführungsdauer Ihrer Methode zu messen, können Sie folgenden Methode verwenden:
1void time(Runnable r) {2final var startTime = System.nanoTime();3r.run();4final var endTime = System.nanoTime();5System.out.println("elapsed time: "+(endTime - startTime));6}
Ein Aufruf der Methode time könnte dann so aussehen:
1 time(() -> System.out.println(sumOfSquares(new int[]{1,2,3,4,5,6,7,8,9,0})));
Lösung mit standard Stream Primitiven (≤ Java 24)
Benötigte Imports
1import static java.util.Comparator.comparingDouble;2import static java.util.function.Predicate.not;3import static java.util.stream.Collectors.groupingBy;4import static java.util.stream.Collectors.minBy;
Geschäftslogik
1List<Student> empfehlungenS = alleStudierenden.stream()2.filter(not(Student::hatStipendium))3.collect(groupingBy(Student::studiengang,4minBy(comparingDouble(Student::schnitt))))5.values().stream().flatMap(Optional::stream)6.sorted(comparingDouble(Student::schnitt))7.toList();
Bewertung
Wir sind näher an der Geschäftslogik, aber technische Artefakte scheinen noch durch!
Um die gewünschte Gruppierung zu erhalten, werden die besten Studierenden in einer Zwischendatenstruktur (Map) aufgesammelt (collect(...)). Diese muss - um eine Stream-orientierte Weiterverarbeitung zu ermöglichen - wieder in einen Stream verwandelt werden, der über den Values der Map operiert (values().stream()).
Da wir in eine Map aufgesammelt hatten, haben wir einen Stream of Optionals; minBy(...) liefert ein Optional. Somit müssen wir die Optionals im Stream<Optional<Student>> über flatMap(Optional::stream) entpacken.
Im Folgenden verwenden wir die Begriffe Custom Intermediate Operations und Stream Gatherers faktisch synonym.
TODODODODODO
Custom Intermediate Operation/Custom Gatherer
1static <T, K> Gatherer<T, ?, T> reducePerGroup(2Function<T, K> grouping, BinaryOperator<T> reducer) {3return Gatherer.ofSequential(4HashMap<K, T>::new,5(map, element, downstream) -> {6map.merge(grouping.apply(element), element, reducer);7return true;8},9(map, downstream) -> map.values().forEach(downstream::push)10); }
1List<Student> empfehlungenG = alleStudierenden.stream()2.filter(not(Student::hatStipendium))3.gather(reducePerGroup(4Student::studiengang,5BinaryOperator.minBy(comparingDouble(Student::schnitt))))6.sorted(comparingDouble(Student::schnitt))7.toList();
benötigte Imports
1import static java.util.Comparator.comparingDouble;2import static java.util.function.Predicate.not;3import static java.util.stream.Collectors.groupingBy;4import static java.util.stream.Collectors.minBy;
Als allgemeine Faustregel gilt, dass Geschwindigkeitssteigerungen in der Regel dann spürbar sind, wenn die Sammlung groß ist, typischerweise mehrere tausend Elemente umfasst.[2]
Übersetzung aus dem Englischen mit Hilfe von DeepL.
(Java) Streams unterstützen die effiziente, korrekte Verarbeitung von (Massen-)Daten durch die Anwendung von funktionalen und deklarativen Konzepten mit dem Ziel einer möglichst geringen Repräsentationslücke (Low representational gap).
Java Streams |
Scala Collections |
|
|---|---|---|
Pipeline-Modell |
Deklarativ, verkettet |
Deklarativ, verkettet |
Funktionen als Parameter |
Lambdas, Method References |
Funktionsliterale, Platzhalter _ |
Quelle unveränderlich |
Ja |
Ja |
Streams = Collections? |
Nein — separates Konzept, explizites .stream() / .toList() |
Ja — Operationen direkt auf Collections |
Auswertung |
Lazy (erst bei Terminal Op) |
Eager (sofort); LazyList als Opt-in |
Erweiterbarkeit |
Collectors seit Java 8, Gatherers seit Java 24 |
'„Nicht nötig“ — umfangreiche Standardbibliothek' |
Parallelisierung |
Tief integriert (.parallelStream()) |
Separate Bibliothek (.par) seit Scala 2.13 |
Scala verfolgt einen anderen Designansatz: Collections sind die Pipeline. Es gibt keine Trennung zwischen Datenstruktur und Transformations-API. Das macht den Einstieg einfacher und den Code kürzer, bedeutet aber auch, dass Laziness explizit gewählt werden muss.
Javas Trennung in Collection und Stream ist verboseer, ermöglicht dafür aber Lazy Evaluation als Standard und eine tief integrierte Parallelisierung.
Java (mit Gatherer)
1alleStudierenden.stream()2.filter(not(Student::hatStipendium))3.gather(reducePerGroup(4Student::studiengang,5BinaryOperator.minBy(6comparingDouble(7Student::schnitt))))8.sorted(comparingDouble(9Student::schnitt))10.toList();
Scala
1alleStudierenden2.filterNot(_.hatStipendium)3.groupBy(_.studiengang)4.values5.map(_.minBy(_.schnitt))6.toList7.sortBy(_.schnitt)
Bewertung
Syntaktisches Rauschen: Java erfordert .stream() / .toList() als Rahmen, Typnamen in Method References (Student::schnitt) und Wrapper wie comparingDouble. Scala's _-Platzhalter und die direkte Arbeit auf Collections eliminieren diesen Overhead.
Gruppierung: In Scala ist groupBy eine normale Collection-Methode — kein Pipeline-Bruch, kein Collector. Java brauchte für denselben Effekt zunächst den collect/Collector-Umweg und seit Java 24 die Gatherer-API.
Vergleich und Reduktion: _.minBy(_.schnitt) in Scala vs. BinaryOperator.minBy(comparingDouble(Student::schnitt)) in Java — die explizite Typmaschinerie des Java-Typsystems ist hier deutlich sichtbar.
Lazy vs. Eager: Scalas Code wird sofort ausgewertet — groupBy erzeugt sofort eine Map. Javas Pipeline hingegen ist vollständig lazy; erst .toList() löst die Berechnung aus. Für große Datenmengen kann das ein relevanter Unterschied sein.