16_Streams
Code-Dateien
| Dateiname | Aktion |
|---|---|
| CODECode_Fahrzeug.zip | Download |
| CODECode_Mitarbeiter.zip | Download |
| CODECode_Student.zip | Download |
PDF-Dokumente
| Dateiname | Aktion |
|---|---|
| PDFFolie_Mitarbeiter.pdf | Öffnen |
| PDFUebung_Speise.pdf | Öffnen |
| PDFUebung_Wohnung.pdf | Öffnen |
Videos
| Dateiname | Aktion |
|---|---|
| VIDEOVideo_Fahrzeug_D | Abspielen |
| VIDEOVideo_Mitarbeiter_D | Abspielen |
| VIDEOVideo_Student_D | Abspielen |
Lernmaterialien
Streams
Java Streams sind eine Art, Daten (z. B. Listen) als “Fluss” zu verarbeiten – Schritt für Schritt, meist in kleinen, gut lesbaren Operationen wie filtern, umwandeln, sortieren und zusammenfassen.
Wichtig: Ein Stream ist keine Datenstruktur, sondern eine Verarbeitungspipeline über Daten.
Grundidee in einem Satz
Du nimmst eine Quelle (z. B. List<Person>), machst
mehrere Verarbeitungsschritte hintereinander (z. B. filter →
map → sorted) und bekommst am Ende ein Ergebnis (z. B.
Liste, Zahl, Boolean).
So ähnlich wie eine Produktionsstraße:
Rohmaterial rein (Quelle)
mehrere Stationen (Operationen)
fertiges Produkt raus (Ergebnis)
Warum Streams nutzen?
✅ Vorteile
Lesbarer als verschachtelte Schleifen (oft)
Weniger Boilerplate (weniger “Zähler”, “if”, “temporäre Listen”)
Deklarativ: Du beschreibst was passieren soll, nicht wie in Schleifen
Einfach parallelisierbar (optional) mit
parallelStream()
⚠️ Typische Missverständnisse
Stream speichert die Elemente nicht neu, er verarbeitet sie nur.
Streams sind oft einmalig: Wenn du einen Stream „verbraucht“ hast, kannst du ihn nicht nochmal benutzen.
Die drei Bausteine eines Streams
(A) Quelle
Zum Beispiel:
liste.stream()Arrays.stream(array)Files.lines(path)(Dateizeilen als Stream)Stream.of(...)
(B) Zwischenoperationen (Intermediate Operations)
Das sind Schritte, die wieder einen Stream zurückgeben, z. B.:
filter(...)→ Elemente auswählenmap(...)→ Elemente umwandelnsorted(...)→ sortierendistinct()→ Duplikate entfernenlimit(n)/skip(n)→ begrenzen/überspringen
Wichtig: Zwischenoperationen werden meist
lazy ausgeführt:
Sie laufen erst wirklich, wenn am Ende eine Endoperation kommt.
(C) Endoperation (Terminal Operation)
Die beendet die Pipeline und erzeugt ein Ergebnis:
collect(...)→ z. B. in Liste/Set/Map sammelnforEach(...)→ ausführen (z. B. drucken)count()→ zählenanyMatch/allMatch/noneMatch()→ prüfenreduce(...)→ zusammenfassen (z. B. Summe)findFirst()/findAny()→ ein Element finden
Beispiele
Beispiel 1: Filtern + Sammeln
Wir wollen alle geraden Zahlen aus einer Liste:
List<Integer> zahlen = List.of(1,2,3,4,5,6);
List<Integer> gerade = zahlen.stream()
.filter(z -> z % 2 == 0)
.toList(); // ab Java 16
// Ergebnis: [2, 4, 6]Beispiel 2: Umwandeln (map)
Wir wollen alle Namen in Großbuchstaben:
List<String> namen = List.of("Anna", "Bob", "Clara");
List<String> gross = namen.stream()
.map(String::toUpperCase)
.toList();
// ["ANNA", "BOB", "CLARA"]Beispiel 3: Filtern + Sortieren + Ergebnis
Namen, die mit A anfangen, alphabetisch:
List<String> result = namen.stream()
.filter(n -> n.startsWith("A"))
.sorted()
.toList();Beispiel 4: Zusammenfassen (count, sum)
Wie viele sind länger als 3 Zeichen?
long anzahl = namen.stream()
.filter(n -> n.length() > 3)
.count();Summe:
int summe = zahlen.stream()
.mapToInt(Integer::intValue)
.sum();Student
sumAlterNameA()
public int sumAlterNameA() {
return students.stream()
.filter(s -> s.getName().startsWith("A"))
.filter(s -> s.getAlter() > 10)
.map(s -> s.getAlter())
.mapToInt(i -> i.intValue())
.sum();
}Die Methode berechnet die Summe der Alter aller Students, die (1) mit “A” im Namen anfangen und (2) älter als 10 sind.
students.stream()
Erzeugt einen Stream aus der Collection students (z. B.
List<Student>).
Ab jetzt wird jeder Student nacheinander durch die Pipeline
“geschickt”.
.filter(s -> s.getName().startsWith("A"))
Lässt nur die Students durch, deren Name mit “A” beginnt.
- Beispiel:
"Anna"✅,"Andreas"✅,"Berta"❌
.filter(s -> s.getAlter() > 10)
Von den “A”-Students bleiben nur jene, deren Alter größer als 10 ist.
Alter 10 ❌ (weil
> 10)Alter 11 ✅
👉 Du könntest die zwei Filter auch in einen zusammenfassen:
.filter(s -> s.getName().startsWith("A") && s.getAlter() > 10)Ist funktional identisch.
.map(s -> s.getAlter())
Jetzt wird aus jedem Student nur noch sein Alter gemacht.
Vorher: Stream
Nachher: Stream (oder Stream von dem Rückgabetyp von
getAlter())
Beispiel: Student("Anna", 12) → 12
.mapToInt(i -> i.intValue())
Wandelt den Stream von Integer-Objekten in einen
IntStream (primitive int) um.
i -> i.intValue()heißt: “mach aus dem Integer den primitiven int”Kürzer ginge auch:
.mapToInt(Integer::intValue)Warum nötig?
Weil sum() nur bei IntStream existiert (nicht
bei Stream<Integer>).
.sum()
Addiert alle int-Werte im IntStream und
liefert die Summe als int.
Angenommen:
Anna (12)
Anton (9)
Berta (20)
Andreas (15)
Pipeline:
Name startet mit A → Anna(12), Anton(9), Andreas(15)
Alter > 10 → Anna(12), Andreas(15)
Alter extrahieren → 12, 15
Summe → 27
alleNamen()
public String[] alleNamen() {
return students.stream()
.map(s -> s.getName())
.map(s -> s.toUpperCase())
.distinct()
.sorted()
.toArray(size -> new String[size]);
}Diese Methode liefert ein String-Array mit allen Namen, dabei werden die Namen groß geschrieben, Duplikate entfernt und alphabetisch sortiert.
students.stream()
Startet einen Stream über alle students (z. B.
List<Student>).
.map(s -> s.getName())
Macht aus jedem Student seinen Namen.
Vorher:
Stream<Student>Nachher:
Stream<String>
.map(s -> s.toUpperCase())
Wandelt jeden Namen in Großbuchstaben um.
Beispiel:
"Anna"→"ANNA""Andreas"→"ANDREAS"
(Das ist nützlich, weil danach distinct() und
sorted() case-unabhängig “einheitlicher”
arbeiten.)
.distinct()
Entfernt Doppelungen (Duplikate) im Stream.
Wichtig: Bei Strings bedeutet das: gleiche Zeichenfolge (nach
toUpperCase()!).
Beispiel:
"Anna"und"ANNA"würden nach Großschreibung beide"ANNA"sein → nur einmal übrig.
.sorted()
Sortiert die verbleibenden Namen aufsteigend
(alphabetisch) nach natürlicher Ordnung
(String-Vergleich).
Ergebnis z. B.:
["ANDREAS", "ANNA", "BERTA"] (je nach Inhalt)
.toArray(size -> new String[size])
Erzeugt am Ende ein String[] in genau der benötigten Größe.
Warum so?
toArray()ohne Argument gibt nurObject[]zurück.Mit
size -> new String[size]sagst du: “Baue mir einString[]mit dieser Länge”.
In Java 17 ist die kürzere Variante üblich:
.toArray(String[]::new)Gleiche Logik, etwas kürzer und lesbarer:
public String[] alleNamen() {
return students.stream()
.map(Student::getName)
.map(String::toUpperCase)
.distinct()
.sorted()
.toArray(String[]::new);
}Lazy Execution
Streams sind “faul”:
Die Zwischenoperationen laufen erst, wenn eine Endoperation kommt.
Stream<Integer> s = zahlen.stream()
.filter(z -> z % 2 == 0); // passiert noch nichts
// Erst jetzt wird wirklich gearbeitet:
long c = s.count();Häufige Operatoren kurz erklärt
filter(x -> ...)→ behält nur Elemente, die die Bedingung erfüllenmap(x -> ...)→ macht aus jedem Element ein neues (Transformation)flatMap(...)→ „Listen von Listen“ zu einer flachen Liste machencollect(Collectors.toList())oder.toList()→ Ergebnis einsammelnreduce(...)→ alles zu einem Wert zusammenfassen (z. B. Produkt, Summe)peek(...)→ zum Debuggen reinschauen (nicht als Logik missbrauchen)
Streams vs. Schleifen – wann was?
Streams sind super, wenn …
du Daten in mehreren Schritten verarbeiten willst (filter → map → collect)
du lesbaren Code möchtest
du Aggregationen brauchst (count/sum/grouping)
Schleifen sind oft besser, wenn …
du sehr komplexe Kontrolllogik hast (break/continue an vielen Stellen)
du extrem performancekritischen Code hast und jede Allokation vermeiden willst
du vieles mit Seiteneffekten machst (z. B. in mehrere Strukturen schreiben)
flatMap
1) flatMap ganz einfach
Problem
Du hast eine Liste von Listen, z. B. Klassen → Schüler:
List<List<String>> klassen = List.of(
List.of("Anna", "Bob"),
List.of("Clara"),
List.of("David", "Eva")
);Wenn du jetzt nur map benutzt:
List<Stream<String>> streams = klassen.stream()
.map(List::stream)
.toList();Dann bekommst du eine Liste von Streams (also „verschachtelt“). Oft willst du aber eine einzige flache Liste aller Namen.
Lösung: flatMap
flatMap bedeutet: umwandeln und dabei
“flachziehen”.
List<String> alleSchueler = klassen.stream()
.flatMap(List::stream) // macht aus List<List<String>> -> Stream<String>
.toList();
System.out.println(alleSchueler);
// [Anna, Bob, Clara, David, Eva]Merksatz:
map= 1 Element → 1 Element (oder 1 Element → 1 Liste/Stream, aber bleibt verschachtelt)flatMap= 1 Element → viele Elemente, und am Ende alles in einem Stream
Mini-Beispiel mit Text
Wörter in einem Satz, aber flach als einzelne Wörter:
List<String> saetze = List.of("Hallo Welt", "Java Streams sind cool");
List<String> woerter = saetze.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.toList();
// ["Hallo", "Welt", "Java", "Streams", "sind", "cool"]Gruppierung
Im Student-Projekt passen Gruppierungen mit Streams richtig gut, weil du viele sinnvolle Schlüssel hast: Klasse, Typ (Tag/Abend), Semester, Alter, Matura, Anzahl Ferialpraxis.
import java.util.*;
import java.util.stream.Collectors;Gruppieren nach
Klasse: Map<String, List<Student>>
Alle Students je Klasse sammeln (z. B. “3AHIF” → Liste)
public Map<String, List<Student>> gruppiereNachKlasse() {
return students.stream()
.collect(Collectors.groupingBy(s -> s.getKlasse()));
}Die Methode baut dir eine Map, in der jede
Klasse (z. B. "3AHIF") der Schlüssel ist und als
Wert die Liste aller Students dieser Klasse steht.
Rückgabetyp
Map<String, List<Student>>
Key (
String): die Klasse, z. B."3AHIF"Value (
List<Student>): alle Students, die zu dieser Klasse gehören
Also ungefähr so:
"3AHIF" → [Student1, Student2, ...]"2BHIF" → [StudentX, StudentY, ...]
students.stream()
Erzeugt einen Stream über alle Students in deiner Collection
students (z. B. eine List<Student>).
collect(...)
collect ist eine Endoperation: Sie
sammelt die Stream-Elemente zu einem Ergebnis (hier: eine Map).
Collectors.groupingBy(...)
groupingBy macht genau das, was der Name sagt:
gruppieren nach einem Schlüssel.
Für jeden Student wird ein Schlüssel berechnet.
Alle Students mit dem gleichen Schlüssel landen gemeinsam in einer Liste.
s -> s.getKlasse()
Das ist die Schlüsselfunktion: Für jeden Student wird seine Klasse als Gruppenschlüssel verwendet.
Du kannst das auch kürzer schreiben als Methodenreferenz:
.collect(Collectors.groupingBy(Student::getKlasse));Mini-Beispiel
Angenommen students enthält:
Anna, Klasse
"3AHIF"Bob, Klasse
"3AHIF"Clara, Klasse
"2BHIF"
Dann liefert die Methode:
"3AHIF"→ [Anna, Bob]"2BHIF"→ [Clara]
Gruppieren
nach Klasse, aber nur Namen:
Map<String, List<String>>
public Map<String, List<String>> namenNachKlasse() {
return students.stream()
.collect(Collectors.groupingBy(
s -> s.getKlasse(),
Collectors.mapping(s -> s.getName(), Collectors.toList())
));
}Diese Methode erstellt eine Map, die pro Klasse eine Liste der Namen enthält.
Also nicht Student-Objekte pro Klasse, sondern nur die
Strings (Namen).
Rückgabetyp: Map<String, List<String>>
Key (
String): Klasse (z. B."3AHIF")Value (
List<String>): Namen aller Students in dieser Klasse
Beispiel:
"3AHIF"→["Anna", "Bob", "Clara"]"2BHIF"→["David", "Eva"]
students.stream()
Startet den Stream über alle Student-Objekte.
collect(...)
Endoperation: sammelt zu einem Ergebnis (hier: eine Map).
Collectors.groupingBy( ... )
groupingBy gruppiert Elemente nach einem Schlüssel.
Es hat hier zwei Teile:
groupingBy(
keyFunction,
downstreamCollector
)Teil A: Schlüssel (Key Function)
s -> s.getKlasse()Für jeden Student wird die Klasse als Schlüssel berechnet.
Damit entscheidet sich, in welche Gruppe der Student
kommt.
Teil B: “Downstream Collector” – was passiert innerhalb jeder Gruppe?
Hier steht:
Collectors.mapping(s -> s.getName(), Collectors.toList())Das heißt: In jeder Klasse-Gruppe soll nicht der ganze Student gesammelt werden, sondern:
mapping: wandle jedes Gruppenelement um
s -> s.getName()macht ausStudenteinenString(den Namen)toList: sammle diese Namen in eine Liste
Kurz gesagt:
➡️ pro Klasse: Student → Name → Liste von Namen
Mit Methodenreferenzen:
public Map<String, List<String>> namenNachKlasse() {
return students.stream()
.collect(Collectors.groupingBy(
Student::getKlasse,
Collectors.mapping(Student::getName, Collectors.toList())
));
}Anzahl
pro Klasse (wie SQL GROUP BY klasse COUNT(*)):
Map<String, Long>
public Map<String, Long> anzahlProKlasse() {
return students.stream()
.collect(Collectors.groupingBy(
Student::getKlasse,
Collectors.counting()
));
}Guppieren
nach Typ (TagStudent vs AbendStudent):
Map<String, List<Student>>
Hier gruppieren wir nach einem selbst gewählten Label:
public Map<String, List<Student>> gruppiereNachTyp() {
return students.stream()
.collect(Collectors.groupingBy(s ->
(s instanceof AbendStudent) ? "AbendStudent" : "TagStudent"
));
}Files
public class StudentException extends RuntimeException {
public StudentException(String meldung) {
super(meldung);
}
}Du baust sie nur dann zu RuntimeException
um, wenn du willst, dass StudentException eine
unchecked Exception ist – also nicht
überall mit throws/try-catch “mitgeschleppt”
werden muss.
Der Kern ist der Unterschied:
extends Exception→ checked exception
Java zwingt dich: entweder fangen oder weiterwerfen (try/catchoderthrows).extends RuntimeException→ unchecked exception
Java zwingt dich nicht – du kannst sie werfen, ohne Methodensignaturen anzupassen.
Bei Streams (z. B. forEach, map) kannst du
nicht einfach eine checked Exception werfen, weil die
Functional Interfaces (Consumer, Function, …)
keine checked Exceptions in der Signatur haben.
public void importSchool(String filename) throws StudentException {
if (filename == null) {
throw new StudentException("Fehler: keine Datei!");
}
try (Stream<String> lines = Files.lines(Path.of(filename))) {
lines.forEach(line -> {
if (line.startsWith("A")) {
aufnehmen(new AbendStudent(line));
} else if (line.startsWith("T")) {
aufnehmen(new TagStudent(line));
}
});
} catch (java.nio.file.NoSuchFileException e) {
throw new StudentException("Fehler: kann die Datei " + filename + " nicht finden!");
} catch (IOException e) {
throw new StudentException("Fehler: kann die Datei " + filename + " nicht lesen!");
}
}Die Methode liest eine Datei zeilenweise ein und erzeugt pro Zeile
(je nach Anfangsbuchstabe) einen AbendStudent oder
TagStudent, den sie dann mit
aufnehmen(...) in deine School übernimmt.
public void importSchool(String filename) throws StudentException {Methode heißt
importSchoolbekommt den Dateinamen als
Stringkann eine
StudentExceptionwerfen (deine eigene Exception-Klasse)
Null-Check
if (filename == null) {
throw new StudentException("Fehler: keine Datei!");
}Wenn filename null ist, ist das ein
Programmfehler / falscher Aufruf.
Dann wird sofort abgebrochen und eine verständliche Fehlermeldung
geworfen.
Datei als Stream lesen (try-with-resources)
try (Stream<String> lines = Files.lines(Path.of(filename))) {Path.of(filename)macht aus dem String einenPath.Files.lines(...)öffnet die Datei und liefert einenStream<String>, also einen Stream aus Zeilen.try-with-resources sorgt dafür, dass die Datei/der Stream am Ende automatisch geschlossen wird – auch wenn ein Fehler passiert.
Das ist die moderne Stream-Alternative zu
FileReader + BufferedReader + while(readLine()).
Jede Zeile verarbeiten
lines.forEach(line -> {
if (line.startsWith("A")) {
aufnehmen(new AbendStudent(line));
} else if (line.startsWith("T")) {
aufnehmen(new TagStudent(line));
}
});forEach(...)ist die Endoperation: sie läuft über alle Zeilen.Für jede Zeile (
line) wird geprüft:Beginnt sie mit
"A"→ AbendStudent erzeugenBeginnt sie mit
"T"→ TagStudent erzeugen
new AbendStudent(line)/new TagStudent(line)bedeutet:
Der Konstruktor liest vermutlich die Information aus der Zeile (z. B. Name, Alter, Klasse …).aufnehmen(...)speichert den Student dann in deiner Sammlungstudents.
Wenn eine Zeile weder mit A noch mit T
beginnt:
Passiert einfach nichts (die Zeile wird ignoriert).
Datei nicht gefunden
} catch (java.nio.file.NoSuchFileException e) {
throw new StudentException("Fehler: kann die Datei " + filename + " nicht finden!");
}Wenn die Datei nicht existiert (oder der Pfad falsch ist), kommt
NoSuchFileException. Du wandelst sie in eine
StudentException mit eigener Meldung um.
Andere Leseprobleme
catch (IOException e) {
throw new StudentException("Fehler: kann die Datei " + filename + " nicht lesen!");
}Das ist alles andere, was beim Lesen schiefgehen kann, z. B.:
keine Berechtigung
Datenträgerfehler
Datei ist gesperrt
etc.
Auch das wird in StudentException umgewandelt, damit
dein Programm einheitlich mit Fehlern umgehen kann.
Warum ist das “Stream”-Code, obwohl es wie eine Schleife wirkt?
Weil:
Files.lines(...)liefert die Zeilen als StreamforEachist die “streamige” Art, über alle Elemente zu laufen
Das ist hier völlig ok, weil du Seiteneffekte hast
(aufnehmen(...)).
Das hier ist identisch, aber kürzer:
try (Stream<String> lines = Files.lines(Path.of(filename))) {
lines.forEach(line -> {
if (line.startsWith("A")) aufnehmen(new AbendStudent(line));
else if (line.startsWith("T")) aufnehmen(new TagStudent(line));
});
}