Alle Tabs der Lerneinheit (Erklärung · Interaktiv verstehen · Praxis-Übung · Klausur-Quiz) als durchgehender Text. Ideal zum Wiederholen vor der Klausur, und für Suchmaschinen wie Google, Bing und KI-Suche (ChatGPT, Perplexity).
Erklärung
In Programmieren 1 hast du map, filter und reduce als Lambdas kennengelernt. Die Stream-API ist das volle Werkzeug dahinter: eine Pipeline, die Daten durch mehrere Operationen schickt, lazy ausgewertet, abgeschlossen mit einer terminalen Operation und mächtigen Collectors zum Gruppieren und Aggregieren. In Programmieren 2 (z.B. TU Berlin, Mannheim) ist genau diese Tiefe Klausurstoff.
Was du können musst:
- die Anatomie einer Pipeline: Quelle, intermediate Operationen, terminale Operation
- Lazy Evaluation und Short-Circuit verstehen (warum nichts ohne Terminal-Op läuft)
- Collectors einsetzen:
toList,groupingBy,joining,counting reduce,flatMapund primitive Streams (IntStream) anwenden- die typischen Stream-Fallen kennen (Einmal-Nutzung, parallele Seiteneffekte)
Voraussetzung: Lambdas, funktionale Interfaces und Method-References (siehe Topic Lambdas & Higher-Order-Funktionen). Hier geht es um die Stream-API darauf aufbauend, nicht um die Lambda-Syntax selbst.
Die Idee in einem Satz
Ein Stream ist keine Datenstruktur, sondern eine Pipeline: eine Quelle, beliebig viele intermediate Operationen (lazy, geben wieder einen Stream zurück) und genau eine terminale Operation (startet die Verarbeitung und liefert ein Ergebnis).
Anatomie einer Pipeline
List<String> namen = List.of("Anna", "Bob", "Cara");
String ergebnis = namen.stream() // Quelle
.filter(s -> s.length() == 4) // intermediate (lazy)
.map(String::toUpperCase) // intermediate (lazy)
.collect(Collectors.joining(", ")); // terminal (startet alles)
// ergebnis = "ANNA, CARA"
| Teil | Beispiele | Eigenschaft |
|---|---|---|
| Quelle | collection.stream(), Stream.of(...), IntStream.range(0, n), Arrays.stream(arr) | erzeugt den Stream |
| intermediate | filter, map, sorted, distinct, limit, flatMap, peek | lazy, geben einen Stream zurück, verkettbar |
| terminal | collect, reduce, forEach, count, findFirst, anyMatch, toList | eager, lösen die Verarbeitung aus, liefern Ergebnis oder void |
Faustregel zum Erkennen: gibt die Methode einen
Streamzurück, ist sie intermediate. Gibt sie etwas anderes zurück (Liste, Zahl,Optional,void), ist sie terminal und beendet die Pipeline.
Lazy Evaluation: ohne Terminal-Op läuft nichts
Die intermediate Operationen bauen nur die Pipeline auf, sie rechnen noch nicht. Erst die terminale Operation zieht die Elemente durch, und zwar Element für Element, nicht Stufe für Stufe. Das erlaubt Short-Circuit: Operationen wie findFirst, anyMatch oder limit stoppen, sobald das Ergebnis feststeht.
Im Stepper siehst du, dass bei findFirst nur das erste passende Element komplett durch die Pipeline läuft, der Rest wird nie angefasst.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Collectors: sammeln, gruppieren, aggregieren
Die terminale Operation collect(...) ist die mächtigste. Sie nimmt einen Collector entgegen, ein Rezept, wie die Elemente zusammengefasst werden:
| Collector | Ergebnis | Wofür |
|---|---|---|
toList() / toSet() | List / Set | Elemente einsammeln |
toMap(k, v) | Map | Schlüssel-Wert-Paare (Duplikat-Key wirft, ohne merge-Funktion) |
groupingBy(classifier) | Map<K, List<V>> | nach einem Kriterium gruppieren |
partitioningBy(predicate) | Map<Boolean, List<V>> | in zwei Gruppen (true/false) teilen |
joining(", ") | String | Strings zusammenfügen |
counting() | Long | zählen |
summingInt / averagingInt | Integer / Double | summieren / mitteln |
groupingBy ist der Klausur-Klassiker. Im Stepper siehst du, wie eine Wörter-Liste nach Länge in eine Map gruppiert wird.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
reduce, flatMap und primitive Streams
reduce kombiniert alle Elemente zu einem Wert. Mit Startwert (identity) bekommst du den Typ direkt, ohne Startwert ein Optional (der Stream könnte leer sein):
int summe = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); // 10
Optional<Integer> max = Stream.of(1, 2, 3).reduce(Integer::max); // Optional[3]
flatMap glättet verschachtelte Strukturen (Stream von Listen wird ein Stream von Elementen):
List<List<Integer>> verschachtelt = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flach = verschachtelt.stream()
.flatMap(List::stream) // aus Stream<List<Integer>> wird Stream<Integer>
.toList(); // [1, 2, 3, 4]
Primitive Streams (IntStream, LongStream, DoubleStream) vermeiden Autoboxing und bieten sum, average, range:
int summe = IntStream.rangeClosed(1, 100).sum(); // 5050
Parallele Streams (stream().parallel()) verteilen die Arbeit auf mehrere Threads, lohnen sich aber nur bei großen, unabhängigen, zustandslosen Operationen, und die Reihenfolge ist dann nicht garantiert.
Klausur-Faustregeln
1. Pipeline = Quelle + intermediate (lazy) + genau eine terminale Op. Ohne terminale Operation passiert gar nichts, der Stream wird nie ausgewertet.
2. Intermediate erkennt man am Rückgabetyp Stream. filter/map/sorted geben einen Stream zurück (verkettbar), collect/count/findFirst liefern ein Ergebnis und beenden die Pipeline.
3. Ein Stream ist einmal nutzbar. Nach der terminalen Operation ist er verbraucht. Erneutes Verwenden wirft IllegalStateException.
4. Reihenfolge der Operationen zählt. filter vor map spart Arbeit (weniger Elemente zu transformieren), sorted ist teuer (braucht den ganzen Stream und ist ein Pipeline-Bremser).
5. groupingBy für Gruppen, toMap für eindeutige Schlüssel. groupingBy ergibt Map<K, List<V>>. toMap wirft bei doppelten Schlüsseln eine Exception, wenn keine merge-Funktion angegeben ist.
6. reduce ohne Startwert gibt Optional. Mit identity (z.B. reduce(0, ...)) bekommst du den Typ direkt; ohne, weil der Stream leer sein könnte, ein Optional.
Häufige Stolpersteine
1. Stream zweimal nutzen. var s = list.stream(); s.count(); s.forEach(...); wirft IllegalStateException: stream has already been operated upon. Pro Stream genau eine terminale Operation.
2. Intermediate ohne Terminal. list.stream().map(x -> teuer(x)); ohne terminale Op tut nichts, weil lazy. Kein Output, kein Seiteneffekt, der map-Code läuft nie.
3. forEach mit collect verwechseln. forEach liefert void (nur Seiteneffekt). Wer ein Ergebnis sammeln will, braucht collect oder reduce, nicht forEach mit manuellem Befüllen einer Liste.
4. toMap mit doppelten Schlüsseln. Ohne merge-Funktion knallt es bei Duplikaten (IllegalStateException: Duplicate key). Lösung: dritter Parameter (a, b) -> a oder groupingBy statt toMap.
5. reduce-Rückgabe falsch erwartet. Ohne identity ist das Ergebnis ein Optional, nicht der Wert direkt. Wer int summe = stream.reduce(Integer::sum) schreibt, bekommt einen Typfehler (es ist Optional<Integer>).
6. Seiteneffekte in parallelen Streams. parallelStream().forEach(e -> liste.add(e)) auf eine normale ArrayList ist eine Race Condition (nicht thread-safe). collect(Collectors.toList()) ist der sichere Weg, parallel-tauglich.
Interaktiv verstehen
Eine echte Pipeline Schritt für Schritt
Eine Liste von Studierenden mit Noten läuft durch eine typische Klausur-Pipeline: bestandene herausfiltern, nach Note sortieren, die Namen extrahieren und zu einem String zusammenfügen. Beobachte, wie sich die Zwischen-Liste nach jeder Stufe verändert, und dass die Reihenfolge der Operationen das Ergebnis bestimmt.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Klausur-Tipp: Bei "Was gibt diese Pipeline zurück?" gehst du Stufe für Stufe vor und notierst die Zwischen-Liste nach jeder Operation. Achte auf den Rückgabetyp der terminalen Op (List, String, Map, long, Optional), das ist oft die eigentliche Frage.
Praxis-Übung
Klausurfragen mit Lösungen (6)
- F1.Welche dieser Operationen ist eine TERMINALE Operation?
Antwort: collect
Erklärung: collect ist terminal: es loest die Verarbeitung aus und liefert ein Ergebnis. filter, map und sorted sind intermediate (lazy) und geben wieder einen Stream zurueck. Erkennungsregel: gibt die Methode einen Stream zurueck, ist sie intermediate.
- F2.Eine Stream-Pipeline ohne terminale Operation (nur filter und map) verarbeitet die Elemente trotzdem.
Antwort: Falsch
Erklärung: Falsch. Intermediate Operationen sind lazy, sie bauen nur die Pipeline auf. Ohne terminale Operation laeuft gar nichts, der map/filter-Code wird nie ausgefuehrt.
Typ: Wahr/Falsch
- F3.Was gibt diese Pipeline zurueck?
Antwort: 3
Erklärung: filter length >= 2 behaelt aa, ccc, dd (b faellt raus). count() zaehlt sie: 3. count ist eine terminale Operation, die einen long zurueckgibt.
- F4.Welcher Collector erzeugt eine Map<Integer, List<String>>, die Woerter nach ihrer Laenge gruppiert?
Antwort: Collectors.groupingBy(String::length)
Erklärung: groupingBy(String::length) ergibt Map<Integer, List<String>>, alle Woerter gleicher Laenge landen in einer Liste. toMap wuerde bei gleicher Laenge (Duplikat-Key) eine Exception werfen; partitioningBy teilt nur in true/false (braucht ein Predicate).
- F5.Ordne jede Operation ihrer Art zu.
Zuordnungen:
- filter → intermediate (lazy)
- collect → terminal
- map → intermediate (lazy)
- findFirst → terminal
Erklärung: filter und map geben einen Stream zurueck (intermediate, lazy). collect und findFirst liefern ein Ergebnis und beenden die Pipeline (terminal).
Typ: Zuordnung
- F6.Warum verarbeitet findFirst hier nur das erste Element, obwohl der Stream vier hat?
Antwort: Wegen Lazy Evaluation und Short-Circuit: findFirst stoppt nach dem ersten Treffer
Erklärung: Lazy Evaluation zieht die Elemente einzeln durch die Pipeline. 1 passt durch filter (ungerade), wird zu 10 gemappt, findFirst nimmt es und schliesst kurz (Short-Circuit). 2, 3, 4 werden nie verarbeitet. Ergebnis: Optional[10].
Stream.of("aa", "b", "ccc", "dd")
.filter(s -> s.length() >= 2)
.count();Stream.of(1, 2, 3, 4)
.filter(n -> n % 2 == 1)
.map(n -> n * 10)
.findFirst();Klausur-Quiz
Klausurfragen mit Lösungen (6)
- F1.Was passiert, wenn man denselben Stream zweimal mit einer terminalen Operation nutzt?
Antwort: IllegalStateException: stream has already been operated upon
Erklärung: Ein Stream ist einmal nutzbar. Nach der ersten terminalen Operation (count) ist er verbraucht, die zweite (forEach) wirft zur Laufzeit IllegalStateException. Loesung: einen neuen Stream aus der Quelle erzeugen.
- F2.Wie unterscheidet man intermediate von terminalen Operationen am sichersten?
Antwort: Intermediate geben einen Stream zurueck, terminale ein Ergebnis oder void
Erklärung: Am Rueckgabetyp: intermediate Operationen geben wieder einen Stream zurueck (verkettbar, lazy). Terminale Operationen geben etwas anderes zurueck (List, long, Optional, void) und loesen die Verarbeitung aus.
- F3.Was ist das Ergebnis?
Antwort: "BB-CC"
Erklärung: filter length == 2 behaelt bb und cc. map toUpperCase ergibt BB und CC. joining mit Trenner - fuegt sie zu BB-CC zusammen.
- F4.Eine Pipeline besteht aus einer Quelle, mehreren {{1}} Operationen (lazy) und genau einer {{2}} Operation. Zum Gruppieren nach einem Kriterium nutzt man den Collector {{3}}.
Lösungen pro Lücke:
- {{1}}: intermediate / intermediären / intermediaeren
- {{2}}: terminalen / terminal
- {{3}}: groupingBy / Collectors.groupingBy
Erklärung: Quelle + intermediate (lazy, geben Stream zurueck) + eine terminale Op (loest aus). groupingBy gruppiert in eine Map<K, List<V>>.
Typ: Lückentext
- F5.reduce ohne Startwert (identity) gibt direkt den Wert zurueck, nicht ein Optional.
Antwort: Falsch
Erklärung: Falsch. Ohne identity gibt reduce ein Optional zurueck, weil der Stream leer sein koennte und es dann keinen Wert gaebe. Mit Startwert (z.B. reduce(0, Integer::sum)) bekommt man den Typ direkt.
Typ: Wahr/Falsch
- F6.Bring die Pipeline in die effizienteste, korrekte Reihenfolge: Namen der bestandenen Studierenden, alphabetisch, als Liste.
Richtige Reihenfolge:
- liste.stream()
- .filter(s -> s.note() <= 4)
- .map(Student::name)
- .sorted()
- .collect(Collectors.toList())
Erklärung: Quelle zuerst. filter vor map (weniger Elemente zu transformieren). sorted nach map (sortiert die Namen alphabetisch). collect als terminale Op am Ende. filter vor sorted ist effizient, weil weniger Elemente sortiert werden muessen.
Typ: Reihenfolge
Stream<Integer> s = Stream.of(1, 2, 3);
long n = s.count();
s.forEach(System.out::println);List.of("a", "bb", "cc", "ddd").stream()
.filter(s -> s.length() == 2)
.map(String::toUpperCase)
.collect(Collectors.joining("-"));