W3docs

Java Stream-Terminaloperationen

Stream-Auswertung in Java mit Terminaloperationen auslösen — collect, forEach, reduce, count, min, max, anyMatch.

Eine Terminaloperation bringt eine Stream-Pipeline tatsächlich zum Laufen. Zwischenoperationen (filter, map, sorted, …) zeichnen nur die Arbeit auf und bleiben lazy; ein Terminal zieht Elemente durch, wertet die gesamte Pipeline aus und erzeugt ein Ergebnis (oder einen Seiteneffekt). Jede Pipeline endet mit genau einem Terminal — wird es aufgerufen, ist der Stream verbraucht; ein weiteres Terminal auf demselben Stream führt zu IllegalStateException.

Dieses Kapitel behandelt alle Terminals, die man schreiben wird, wann jedes davon kurzschließt und die Randfälle mit leeren Streams, über die man leicht stolpert.

Terminals gibt es in drei Formen. Aggregatoren geben einen einzelnen Wert zurück (count, sum, min, max, reduce). Sucher suchen ein Element und hören auf (findFirst, findAny, anyMatch, allMatch, noneMatch). Builder materialisieren den Stream in einen Container (toList, toArray, collect, forEach für Seiteneffekte). Dieses Kapitel behandelt alle Terminals außer collect, das groß genug für ein eigenes Kapitel ist.

forEach / forEachOrdered — Seiteneffekte

Das einfachste Terminal. Führt einen Consumer<T> für jedes Element aus und gibt nichts zurück:

names.stream().forEach(System.out::println);

Die Reihenfolge ist nicht garantiert — bei einem sequenziellen Stream in der Regel schon; bei einem parallelen Stream nicht. Wenn die Quellreihenfolge auch bei paralleler Ausführung benötigt wird, verwendet man forEachOrdered:

names.parallelStream().forEachOrdered(System.out::println);

forEach ist für Seiteneffekte gedacht, die man wirklich möchte — Logging, Mutieren einer Senke, Aufrufen einer Nicht-Stream-API. Es ist nicht der richtige Weg, um eine Collection aufzubauen (dafür ist toList / collect zuständig) oder einen Wert zu akkumulieren (dafür ist reduce zuständig). Ein forEach, das eine äußere Liste mutiert, ist ein Code-Smell, selbst wenn es funktioniert, weil es alles aufgibt, was die Pipeline deklarativ gemacht hat.

count — wie viele Elemente

Gibt einen long zurück:

long adults = people.stream().filter(p -> p.age() >= 18).count();

count kurzschließt bei Quellen mit bekannter Größe, wo die JVM die Antwort aus der Quelllänge berechnen kann (so gibt IntStream.range(0, 1_000_000).count() 1000000 zurück, ohne zu iterieren). Bei einem Stream mit einem aktiven filter oder flatMap muss jedes Element durchlaufen werden.

Eine häufige Falle: stream.count() auf einer .peek(...) Kette läuft den Peek möglicherweise nicht aus, wenn die JVM die Anzahl aus der Quelle ableiten kann, da es keinen beobachtbaren Verhaltensunterschied gibt. peek sollte nicht verwendet werden, um "zu sehen, wie viele gefiltert wurden" — stattdessen mapToInt(x -> 1).sum() verwenden oder umstrukturieren.

min / max — Extremelemente

Beide nehmen einen Comparator<T> und geben Optional<T> zurück (weil der Stream leer sein könnte):

Optional<Person> oldest  = people.stream().max(Comparator.comparingInt(Person::age));
Optional<String> shortest = words.stream().min(Comparator.comparingInt(String::length));

Primitive Spezialisierungen sind einfacher — IntStream.max() gibt OptionalInt zurück, kein Comparator nötig:

OptionalInt highest = nums.stream().mapToInt(Integer::intValue).max();
int hi = highest.orElse(Integer.MIN_VALUE);

min/max kurzschließen nur bei begrenzten Quellen. Auf einem unendlichen Stream terminiert max nie.

findFirst / findAny — ein Element holen

Beide geben Optional<T> zurück, beide kurzschließen. Der Unterschied liegt darin, was sie über welches Element versprechen:

Optional<Person> first = people.stream().filter(p -> p.age() >= 30).findFirst();
Optional<Person> any   = people.stream().filter(p -> p.age() >= 30).findAny();
  • findFirst gibt das erste Element in der Begegnungsreihenfolge zurück. Bei einem sequenziellen Stream ist das das buchstäblich erste. Bei einem parallelen Stream kostet es mehr als findAny, weil die JVM koordinieren muss.
  • findAny gibt irgendein passendes Element zurück — das erste, das ein Worker findet. Parallel ist es günstiger. Sequenziell geben beide dasselbe zurück.

findAny wird verwendet, wenn es wirklich keine Rolle spielt, welches Treffer man erhält (es ist eine einzelne Existenzprüfung, die den Wert benötigt, nicht nur ein boolean). findFirst wird verwendet, wenn man das erste Element meint.

anyMatch / allMatch / noneMatch — Existenzquantoren

Nehmen ein Predicate<T> und geben boolean zurück. Alle drei kurzschließen:

boolean hasAdult  = people.stream().anyMatch(p -> p.age() >= 18);
boolean allAdult  = people.stream().allMatch(p -> p.age() >= 18);
boolean noChildren = people.stream().noneMatch(p -> p.age() < 13);
  • anyMatch hört auf, sobald ein Element passt.
  • allMatch hört auf, sobald ein Element nicht passt.
  • noneMatch ist !anyMatch(p) — hört beim ersten Treffer auf und gibt false zurück.

Leerer-Stream-Semantik (die Regel, über die jeder einmal stolpert): anyMatch auf einem leeren Stream ist false. allMatch und noneMatch auf einem leeren Stream sind beide true — vakuos wahr, weil es keine Gegenbeispiele gibt. Das kann genau das sein, was man will, oder genau das, was man nicht will — je nach Fragestellung. Wenn "leer" eine Möglichkeit ist, die es zu behandeln gilt, sollte man zuerst isEmpty (oder count() == 0) prüfen.

reduce — auf einen einzelnen Wert falten

Der allgemeinste Aggregator. Drei Überladungen, jede für eine etwas andere Form:

Zweiargumentiges reduce(identity, accumulator) — Falten mit einem Startwert, gibt T zurück (kein Optional, weil identity die Antwort für einen leeren Stream ist):

int sum = nums.stream().reduce(0, Integer::sum);
String all = words.stream().reduce(\"\", String::concat);

Einargumentiges reduce(accumulator) — kein Identity; gibt Optional<T> für den leeren-Stream-Fall zurück:

Optional<Integer> sum = nums.stream().reduce(Integer::sum);
Optional<String> longest = words.stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);

Dreiargumentiges reduce(identity, accumulator, combiner) — wird verwendet, wenn der Akkumulator einen anderen Typ als die Elemente erzeugt (und bei paralleler Ausführung erforderlich ist). Der combiner führt zwei Teilergebnisse zusammen:

int totalLength = words.stream()
    .reduce(0,
            (acc, w) -> acc + w.length(),     // BiFunction<Integer, String, Integer>
            Integer::sum);                     // BinaryOperator<Integer>

Drei Regeln für reduce, die verhindern, dass die Pipeline subtil falsch läuft:

  1. Der Akkumulator muss assoziativ sein: f(f(a, b), c) == f(a, f(b, c)). Summen und String-Concat erfüllen das; Subtraktion nicht.
  2. Identity muss eine echte Identität sein: f(id, x) == x für alle x. 0 für +, 1 für *, \"\" für concat.
  3. Akkumulator und Combiner müssen zustandslos und frei von Seiteneffekten sein.

Wird eine dieser Regeln verletzt, liefert eine sequenzielle Pipeline meistens noch das richtige Ergebnis — eine parallele überrascht. (Das ist derselbe Vertrag, auf den sich Collectors.reducing und paralleles reduce stützen.)

sum / average — primitive Aggregatoren

Nur auf primitiven Streams. sum gibt den primitiven Wert zurück; average gibt ein OptionalDouble zurück:

int total      = IntStream.rangeClosed(1, 100).sum();
OptionalDouble avg = nums.stream().mapToInt(Integer::intValue).average();
double mean = avg.orElse(0.0);

Für umfangreichere numerische Zusammenfassungen — count, sum, min, max, average in einem Durchlauf — siehe IntSummaryStatistics:

IntSummaryStatistics stats = nums.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println(stats);   // {count=N, sum=..., min=..., average=..., max=...}

Das ist ein Durchlauf, eine Allokation und deutlich günstiger als die einzelne Berechnung jedes Werts.

toArray und toList — materialisieren

Zwei Kurzschluss-"Gib-mir-alles"-Terminals:

Object[] anyArr = stream.toArray();                     // Object[]
String[] strArr = stream.toArray(String[]::new);        // typed via constructor ref
List<String> immutable = stream.toList();               // Java 16+, unmodifiable

stream.toList() (Java 16+) ist der moderne Weg, einen Stream in eine List zu materialisieren, und in 95% der Fälle die richtige Wahl. Sie ist unveränderlich und darf nulls enthalten; wenn eine veränderliche Liste, eine bestimmte Implementierung oder ein Set/Map benötigt wird, verwendet man collect(Collectors.toCollection(ArrayList::new)) oder dessen Verwandte im nächsten Kapitel.

toArray(T[]::new) ist der einzige Weg, ein typisiertes Array aus einem Objekt-Stream zu erhalten — die IntFunction<T[]> Form gibt der Laufzeit den Komponententyp des Arrays.

iterator und spliterator — Notausgänge

Ein Stream kann in einen Iterator<T> oder Spliterator<T> umgewandelt werden, um ihn an Code weiterzugeben, der einen erwartet:

for (Iterator<String> it = stream.iterator(); it.hasNext(); ) {
    use(it.next());
}

Beide sind Terminals — sie verbrauchen den Stream. Sie existieren für Interoperabilität, nicht für "Ich möchte eine for-Schleife"; wenn man eine Schleife möchte, sollte man eine verwenden, ohne zuerst einen Stream zu erstellen.

Kurzschluss vs. Verbrauch — die Sicherheitstabelle

TerminalKurzschluss bei unendlicher Quelle?
findFirst / findAnyja
anyMatch / allMatch / noneMatchja
limit(n) (Zwischenoperation) dann beliebigja
forEach / forEachOrderednein — verbraucht alles
countnein — verbraucht alles
min / maxnein — verbraucht alles
reducenein — verbraucht alles
sum / average / summaryStatisticsnein — verbraucht alles
toList / toArray / collectnein — verbraucht alles

Das Muster ist klar: Jedes Terminal, das jedes Element betrachten muss, um seine Antwort zu erzeugen, kurzschließt nicht, und die Kombination mit einer unendlichen Quelle ohne vorgelagertes limit hängt die JVM auf. Sucher und Quantoren sind die einzigen "sicheren auf unendlich"-Terminals.

Ein Praxisbeispiel: alle Terminal-Formen in einer Pipeline

Das Programm unten baut einen kleinen Stream, ruft jeden Terminal auf, den wir besprochen haben, und zeigt die leeren-Stream-Antworten für die drei Matcher sowie für min / findFirst / reduce.

java— editable, runs on the server

Was man aus dem Ergebnis mitnehmen kann:

  • Die "Such"-Terminals — findFirst, findAny, anyMatch, allMatch, noneMatch — und die "Alles-verbrauchenden"-Terminals — count, min/max, reduce, sum, toList — teilen das Kapitel klar auf. Die Such-Terminals kurzschließen; die alles-verbrauchenden nicht. Die zweite Gruppe sollte nur mit einer unendlichen Quelle hinter einem limit kombiniert werden.
  • allMatch auf einem leeren Stream gab true zurück. Ebenso noneMatch. Das ist vakuose Wahrheit — die Standardantwort und der häufigste Grund, warum Produktionscode einen leeren-Eingabe-Randfalls "fälschlicherweise besteht". Wenn leer bedeutungsvoll ist, sollte man zuerst darauf prüfen.
  • Die drei reduce-Überladungen decken drei Muster ab. Zweiargumentig mit einer echten Identität gibt T zurück. Einargumentig gibt Optional<T> zurück, weil es keine Identität gibt. Dreiargumentig erlaubt es, dass der Akkumulator-Typ vom Elementtyp abweicht — und das ist die Form, die tatsächlich sicher in paralleler Ausführung ist, weil der Combiner der JVM sagt, wie Teilergebnisse zusammenzuführen sind.
  • summaryStatistics() hat in einem Durchlauf das getan, was das separate Aufrufen von min, max, sum, average und count in fünf Durchläufen getan hätte. Bei jedem nicht-trivialen numerischen Stream sollte man es bevorzugen.
  • toList() hat eine unveränderliche Liste zurückgegeben. Das ist der Java 16+-Standard und fast immer das, was man will; das nächste Kapitel zeigt die Collectors.toCollection(...) Form für den Fall, dass eine veränderliche, eine bestimmte Implementierung oder ein Set / Map benötigt wird.

Was als Nächstes kommt

collect ist das Terminal, das wir aufgeschoben haben — und das Tor zu einer Hälfte der API. Das nächste Kapitel, Java Stream Collectors, behandelt die Collectors-Toolbox: toList/toSet/toMap, groupingBy, partitioningBy, joining, counting, summingInt, averagingDouble, mapping, reducing und das Downstream-Muster, das sie kombiniert.

Übungen

Übung
Ein Stream enthält null Elemente. Welches davon gibt `true` zurück?
Ein Stream enthält null Elemente. Welches davon gibt `true` zurück?
Was this page helpful?