1. Umwandlung List → Set und zurück
Erinnern wir uns kurz, wozu Umwandlungen von Collections überhaupt nötig sind. Häufig brauchen wir in realen Aufgaben:
- Aus einer Liste die eindeutigen Elemente erhalten (z. B. Liste von E-Mails → Menge eindeutiger Adressen).
- Eine Map aufbauen (Map), z. B. aus einer Liste von Namen eine Karte „Name → Namenslänge“ erhalten.
- Elemente zu einem String zusammenfügen (z. B. für eine schöne Ausgabe).
Früher musste man dafür viel Code mit Schleifen, Bedingungen und temporären Collections schreiben. Mit der Stream API ist alles einfacher und stilvoller geworden!
Beispiel: eine Menge eindeutiger Namen aus einer Liste erhalten
Nehmen wir an, wir haben eine Liste von Namen (vielleicht hat sich jemand in eurer Anwendung zweimal eingetragen – kommt vor!):
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Unsere Aufgabe ist es, eine Collection zu erhalten, in der jeder Name nur einmal vorkommt, also eine Menge (Set). Mit der Stream API geht das praktisch in einer einzigen Zeile:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Ausgabe:
[Mariya, Ivan, Anna, Sergey]
(Die Reihenfolge in einem Set ist nicht garantiert – wundern Sie sich nicht, wenn sie bei Ihnen anders ist.)
Und zurück: Set → List?
Manchmal braucht man es umgekehrt – eine Menge in eine Liste verwandeln (z. B. um zu sortieren oder per Index zuzugreifen):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Umwandlung in eine Map: Collectors.toMap()
Beispiel: aus einer Namensliste eine Map „Name → Namenslänge“ erhalten
Manchmal möchte man nicht nur programmieren, sondern richtige Karten bauen! Probieren wir’s aus:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // Schlüssel — der Name selbst
name -> name.length() // Wert — die Länge des Namens
));
System.out.println(nameToLength);
Ausgabe:
{Mariya=5, Ivan=4, Anna=4, Sergey=6}
Wichtig: doppelte Schlüssel
Wenn es in der Ausgangsliste identische Namen gibt, tritt beim Versuch, sie in eine Map zu sammeln, ein Fehler IllegalStateException: Duplicate key auf. Java mag es nicht, wenn Sie zwei Werte unter demselben Schlüssel ablegen.
Wie Duplikate behandeln?
Man kann angeben, was bei Schlüsselkonflikten passieren soll – z. B. den ersten oder den letzten Wert beibehalten:
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // erstes Vorkommen beibehalten
));
System.out.println(nameToLength);
Jetzt stürzt das Programm nicht ab, und in die Map gelangt nur das erste Vorkommen jedes Namens.
Beispiel: Map mit Objekten
Etwas anspruchsvoller: Wir haben eine Liste von Benutzern und möchten eine Map „Name → Benutzer“ aufbauen:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Beispiel einer Benutzerliste
List<User> users = List.of(
new User("Anna", 25),
new User("Sergey", 30),
new User("Mariya", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Ausgabe:
{Mariya=Mariya (22), Anna=Anna (25), Sergey=Sergey (30)}
3. Zu einem String zusammenfügen: Collectors.joining()
Manchmal möchte man nicht nur eine Collection sammeln, sondern einen schönen String für die Benutzer- oder Log-Ausgabe erzeugen. Zum Beispiel alle Namen mit Komma getrennt zusammenfügen:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Ausgabe:
Anna, Sergey, Mariya, Ivan
Man kann Präfix und Suffix hinzufügen
String result = names.stream()
.collect(Collectors.joining(", ", "Liste: [", "]"));
System.out.println(result);
Ausgabe:
Liste: [Anna, Sergey, Mariya, Ivan]
4. Terminaloperationen: forEach, collect, count, anyMatch, allMatch, noneMatch
Methode forEach
Mit forEach sind wir bereits vertraut: Diese Operation führt für jedes Element des Streams eine Aktion aus.
names.stream().forEach(name -> System.out.println("Hallo, " + name + "!"));
Methode collect
Sammelt Elemente in einer Collection, einem String oder einer anderen Struktur. Am häufigsten ist das Sammeln in eine List oder ein Set mit Collectors.toList() bzw. Collectors.toSet().
Methode count
Zählt die Anzahl der Elemente im Stream.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Anzahl der Namen länger als 4 Buchstaben: " + count);
Methoden anyMatch, allMatch, noneMatch
Prüfen, ob die Bedingung für mindestens ein Element (anyMatch), für alle (allMatch) oder für keines (noneMatch) erfüllt ist.
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Gibt es einen kurzen Namen? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Sind alle Namen länger als 3 Buchstaben? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Ivan"));
System.out.println("Gibt es keinen Ivan? " + noneIvan);
Ausgabe:
Gibt es einen kurzen Namen? false
Sind alle Namen länger als 3 Buchstaben? true
Gibt es keinen Ivan? false
5. Terminal- und Zwischenoperationen: Begriffe festigen
Zwischenoperationen (filter, map, distinct, sorted, limit, skip, peek) — geben einen neuen Stream zurück; man kann sie zu Ketten verbinden.
Terminaloperationen (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — beenden den Stream; danach gibt es kein weiteres Ergebnis!
Beispielkette:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Ausgabe:
[ANNA, IVAN, MARIYA, SERGEY]
6. Typische Fehler bei der Umwandlung von Collections mit Streams
Fehler Nr. 1: Keine Behandlung doppelter Schlüssel in toMap
Wenn in der Ausgangs-Collection doppelte Schlüssel vorkommen und Sie Collectors.toMap() ohne expliziten Merge-Funktion verwenden, wirft das Programm eine Exception. Geben Sie in solchen Fällen immer eine Zusammenführungsfunktion an:
// den letzten Wert beibehalten
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Fehler Nr. 2: Verwendung von forEach statt collect
Manche Einsteiger versuchen, eine Collection mit forEach zu „sammeln“, zum Beispiel:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Funktioniert, ist aber nicht der Stream-Way!
Benutzen Sie besser collect(Collectors.toList()) — das ist sicherer und sauberer.
Fehler Nr. 3: Versuch, einen Stream erneut zu verwenden
Einen Stream kann man nur einmal verwenden. Nach einer Terminaloperation (z. B. collect, forEach) führt der Versuch, mit demselben Stream weiterzuarbeiten, zu einer IllegalStateException.
Fehler Nr. 4: Verstoß gegen das Prinzip „ohne Nebenwirkungen“
Zwischenoperationen sollten „rein“ sein (ohne Veränderung externer Variablen). Innerhalb von map oder filter sollte man nichts außerhalb des Streams verändern.
Fehler Nr. 5: Reihenfolge in Set und Map nicht berücksichtigt
Wenn die Reihenfolge der Elemente wichtig ist, verwenden Sie passende Collections — z. B. LinkedHashSet, TreeMap — und wählen Sie den passenden Collector.
GO TO FULL VERSION