Menu

Java Streams: filter, map, collect und reduce erklärt

Wie man Collections mit Javas Stream-API verarbeitet - filter, map, sorted, collect, count und reduce - und dabei lesbare Pipelines statt manueller Schleifen baut.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Pipelines statt Schleifen

Ein Stream ist eine Pipeline zur Verarbeitung einer Folge von Werten. Du beginnst mit einer Quelle - üblicherweise einer Collection - verkettest Operationen wie „behalte nur diese", „transformiere jeden", „sortiere sie" und schließt ab, indem du das Ergebnis einsammelst. Statt eine Schleife mit einer temporären Liste und einem if darin zu schreiben, beschreibst du, was du willst, und überlässt dem Stream das Durchlaufen.

Jetzt, da du Lambdas hast, ergeben Streams Sinn: Jede Operation nimmt ein Lambda (oder eine Methodenreferenz) entgegen, das die Arbeit für ein einzelnes Element beschreibt.

Lies es von oben nach unten: nimm die Namen, behalte die langen, wandle sie in Großbuchstaben um, sammle sie in einer Liste. Die ursprüngliche names-Liste wird nie verändert - ein Stream erzeugt ein neues Ergebnis und lässt die Quelle unangetastet.

Die Anatomie eines Streams

Jede Pipeline hat drei Teile:

  • Eine Quelle - list.stream(), Arrays.stream(array) oder Stream.of(a, b, c).
  • Null oder mehr Zwischenoperationen - filter, map, sorted, distinct, limit. Jede gibt einen weiteren Stream zurück, sodass du sie verketten kannst.
  • Genau eine terminale Operation - collect, count, forEach, reduce, findFirst. Sie stößt die Arbeit an und erzeugt ein Ergebnis (oder einen Seiteneffekt).

Diese Aufteilung ist wichtig wegen einer Regel, die anfangs alle überrascht.

Zwischenoperationen sind verzögert (lazy)

Zwischenoperationen tun von sich aus nichts. Sie merken sich nur, was passieren soll. Die Pipeline läuft erst, wenn eine terminale Operation ein Ergebnis verlangt.

Führe es aus: Das map-Lambda wird nie ausgeführt. Füge eine terminale Operation hinzu, und die ganze Kette erwacht zum Leben:

Diese Verzögerung ist keine Eigenheit, gegen die man ankämpfen müsste - sie erlaubt dem Stream, Operationen zu verschmelzen und unnötige Arbeit zu überspringen. Aber sie bedeutet, dass eine Pipeline ohne terminale Operation nichts tut, was ein häufiger „Warum passiert nichts?"-Bug ist.

filter und map: Behalten und Transformieren

Diese beiden tragen den Großteil der Last. filter nimmt einen Predicate (ein Lambda, das einen boolean zurückgibt) und behält die Elemente, die durchkommen. map nimmt eine Function und ersetzt jedes Element durch das Ergebnis ihrer Anwendung.

Beachte, dass map den Elementtyp ändern kann: Hier wird aus einem Stream von String ein Stream von Integer. Das String::length ist eine Methodenreferenz - eine Kurzform für das Lambda w -> w.length().

Terminale Operationen erzeugen ein Ergebnis

Sobald du den Stream geformt hast, verwandelt ihn eine terminale Operation in etwas Konkretes:

Gängige Terminaloperationen: collect (in eine Liste/Menge/Map einsammeln), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max und reduce. Nachdem eine Terminaloperation gelaufen ist, ist der Stream verbraucht - du kannst ihn nicht wiederverwenden. Rufe list.stream() erneut auf, um eine frische Pipeline zu erhalten.

Ergebnisse einsammeln

collect mit Collectors ist das Arbeitspferd zum Aufbau eines Ergebnisses. Das gängigste ist Collectors.toList():

Collectors bietet dir außerdem toSet(), joining(...), groupingBy(...) und counting(). Ab Java 16 kannst du collect(Collectors.toList()) durch das kürzere .toList() ersetzen (es gibt eine nicht veränderbare Liste zurück):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted, distinct und limit

Diese Zwischenoperationen formen den Stream um, bevor du ihn einsammelst:

sorted() ohne Argument verwendet die natürliche Ordnung; übergib einen Comparator (hier ein Lambda) für eine eigene Reihenfolge. Comparator.reverseOrder() ist die sauberere Art, absteigend zu sortieren.

reduce: Einen Stream zu einem einzigen Wert falten

Wenn du alle Elemente zu einem einzigen Ergebnis zusammenfassen musst - eine Summe, ein Produkt, der längste String - ist reduce das allgemeine Werkzeug. Du gibst ihm einen Startwert und eine Funktion, die zwei Werte zusammenführt:

Für einfache Summen und Durchschnitte sind die spezialisierten Streams klarer: nums.stream().mapToInt(Integer::intValue).sum(). Greife zu reduce, wenn es keinen fertigen Aggregator gibt.

Streams ersetzen nicht jede Schleife

Streams glänzen, wenn du eine Collection in ein Ergebnis transformierst. Sie sind nicht automatisch schneller als eine Schleife, und sie werden umständlich, wenn du externen Zustand verändern oder auf knifflige Weise vorzeitig abbrechen musst. Eine gute Regel: Liest sich die Pipeline wie ein klarer Satz, verwende einen Stream; greifst du nach einem gemeinsam genutzten Zähler oder einem index, ist eine einfache for-Schleife ehrlich und völlig in Ordnung.

Denk außerdem daran, dass ein Stream nur einmal verwendbar ist. Dies löst eine Exception aus:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

Als Nächstes: Optional

Mehrere Stream-Terminaloperationen - findFirst, min, max, reduce ohne Identität - finden möglicherweise nichts, also geben sie keinen nackten Wert zurück. Sie geben einen Optional zurück, Javas Container für „vielleicht ein Wert, vielleicht nichts", der dir endlich eine saubere Alternative dazu bietet, null zurückzugeben. Das ist die nächste Seite.

Häufig gestellte Fragen

Was ist ein Stream in Java?

Ein Stream ist eine Pipeline zur Verarbeitung einer Folge von Elementen - üblicherweise aus einer Collection - über eine Kette von Operationen wie filter, map und sorted, die mit einer terminalen Operation wie collect oder count endet. Er speichert keine Daten und verändert die Quelle nicht; er beschreibt eine Berechnung. Du erstellst einen mit list.stream() und liest die Kette von oben nach unten wie ein Rezept.

Wie wandelt man einen Stream in Java wieder in eine Liste um?

Beende die Pipeline mit einem terminalen Collector: list.stream().filter(...).collect(Collectors.toList()). Ab Java 16 kannst du das kürzere .toList() verwenden, das eine nicht veränderbare Liste zurückgibt. Ohne eine terminale Operation läuft überhaupt nichts, weil Zwischenoperationen wie filter und map verzögert (lazy) ausgewertet werden.

Wann sollte man einen Stream statt einer for-Schleife verwenden?

Greife zu einem Stream, wenn du eine Collection in ein Ergebnis transformierst oder filterst - die Pipeline liest sich wie ein klarer Satz ("nimm die Namen, behalte die langen, wandle sie in Großbuchstaben um, sammle sie in einer Liste"). Bleib bei einer einfachen for-Schleife, wenn du externen Zustand verändern, auf komplizierte Weise vorzeitig abbrechen musst oder die Logik als imperative Schritte einfacher ist. Bei Streams geht es um Klarheit, nicht um rohe Geschwindigkeit.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S