1. Einleitung
Stellen wir uns vor, wir haben eine Liste von Studierenden, und jede Person hat eine Liste ihrer Hobbys. Zum Beispiel:
List<List<String>> hobbies = List.of(
List.of("Schwimmen", "Schach"),
List.of("Fußball"),
List.of("Programmierung", "Lesen", "Kino")
);
Deine Aufgabe: eine einheitliche Liste aller Hobbys erhalten, um zu sehen, wofür sich die Studierenden interessieren. Naheliegend wäre es, map zu verwenden:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
Was haben wir bekommen? Eine Liste von Streams! Also Stream<Stream<String>>. Das ist nicht ganz das, was wir brauchen – wir möchten einfach einen Stream<String> erhalten, um direkt mit jedem Hobby zu arbeiten.
Stell dir vor, du hast eine Kiste, in der weitere Kisten mit Spielzeug liegen. Die Methode map holt jede Kiste heraus und zeigt dir die Kiste (Stream<Stream<String>>). Du möchtest aber gleich alle Spielzeuge sehen (Stream<String>), ohne in den Kisten wühlen zu müssen.
2. flatMap: wie man verschachtelte Kollektionen „entrollt“
Um die „Kisten zu entpacken“ und einen flachen Stream zu erhalten, braucht man flatMap.
Es nimmt eine Funktion entgegen, die für jedes Element einen Stream zurückgibt, und „verflacht“ alle Teil-Streams zu einem einzigen.
Syntax von flatMap
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Einfacher gesagt: flatMap erwartet, dass du für jedes Element einen Stream zurückgibst, und „glättet“ alles zu einem großen Stream.
Beispiel: alle Hobbys der Studierenden zusammenführen
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Schwimmen, Schach, Fußball, Programmierung, Lesen, Kino]
Für jede Hobbysliste haben wir list.stream() aufgerufen (ein Stream der Hobbys eines Studierenden). Und flatMap hat alle Streams zu einem einzigen zusammengeführt – jetzt sehen wir alle Hobbys und nicht die Streams von Hobbys.
Visuelles Schema
hobbies.stream()
|
|---> [Schwimmen, Schach] -> stream
|---> [Fußball] -> stream
|---> [Programmierung, ...] -> stream
|
flatMap: fasst alles zu einem Stream<String> zusammen
Warum löst map die Aufgabe nicht?
Wenn wir map verwenden, erhalten wir Stream<Stream<String>>, und damit ist die Arbeit umständlich: Man kann nicht sofort alle Strings durchlaufen, sondern braucht zusätzliche Schleifen.
3. Praktische Beispiele für den Einsatz von flatMap
Zeichen aus Strings extrahieren (List<String> → Stream<Character>)
Angenommen, du hast eine Liste von Strings und möchtest einen Stream aller Zeichen erhalten, die in allen Strings vorkommen:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
Erklärung:
- word.chars() gibt einen IntStream (Stream von Zeichencodes) zurück.
- mapToObj wandelt die Codes in Zeichen um.
- flatMap vereinigt alle entstehenden Zeichen-Streams zu einem einzigen.
Arbeiten mit Optional: Stream<Optional<T>> → Stream<T>
Angenommen, du hast eine Liste von Optional-Objekten und möchtest nur die tatsächlich vorhandenen Werte als Stream erhalten:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
Hinweis:
Optional<T> besitzt (seit Java 9) die Methode stream(), die entweder einen leeren Stream oder einen Ein-Element-Stream zurückgibt. flatMap vereinigt alle nicht-leeren Werte zu einem Stream.
4. Die neue Methode mapMulti: wann flatMap nicht nötig ist
Warum wurde mapMulti eingeführt?
Die seit Java 16 verfügbare Methode mapMulti funktioniert ähnlich wie flatMap, ist aber etwas effizienter und flexibler.
flatMap ist mächtig, hat aber einen Nachteil: Für jedes Element musst du einen neuen Stream erstellen, selbst wenn du 0, 1 oder mehrere Elemente zurückgeben möchtest. Das kann ineffizient sein, insbesondere wenn du Elemente nur „entrollen“ willst, ohne Zwischenkollektionen oder -streams zu erzeugen.
mapMulti ist die verbesserte Version von flatMap, die es ermöglicht, gewünschte Werte direkt über einen Consumer in den Ergebnis-Stream zu schreiben, ohne Zwischenstrukturen zu erzeugen. Anstatt einen Stream zurückzugeben, gibst du direkt an, welche Elemente über den Consumer in den Ergebnis-Stream aufgenommen werden.
Syntax von mapMulti
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
Für jedes Element wird die Funktion mapper aufgerufen, die das Element selbst und einen Consumer erhält, in den man neue Werte „einspeisen“ kann.
Beispiel: Filtern und Entrollen der Elemente in einem Durchgang
Angenommen, du hast eine Liste von Zahlen und möchtest bei geraden Zahlen diese zweimal in den Stream aufnehmen, bei ungeraden – gar nicht (also Filtern und „Entrollen“ gleichzeitig):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // Zweimal hinzufügen
}
// Für ungerade Zahlen nichts tun (Filterung)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Vergleich mit flatMap
Die gleiche Aufgabe mit flatMap müsste so geschrieben werden:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Hier müssen wir dennoch für jedes Element Stream.of(...) oder Stream.empty() erzeugen, selbst wenn das ineffizient ist.
5. Wann flatMap und wann mapMulti verwenden?
- map – wenn du jedes Element in genau ein Element transformierst.
- flatMap – wenn du jedes Element in einen Stream von Elementen transformierst.
- mapMulti – wenn du mehrere Elemente erzeugen willst, ohne einen Zwischen-Stream zu erstellen (effizienter, insbesondere in heißen Schleifen).
Weiteres Beispiel: Map<Integer, List<String>> in Stream<Pair(Integer, String)> entrollen
Angenommen, wir haben eine Map: id → Liste von Hobbys.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Schwimmen", "Schach"),
2, List.of("Fußball"),
3, List.of("Programmierung", "Lesen", "Kino")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Schwimmen, Schach, Fußball, Programmierung, Lesen, Kino]
Und wenn wir Paare (id, Hobby) erhalten möchten:
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Schwimmen, 1: Schach, 2: Fußball, 3: Programmierung, 3: Lesen, 3: Kino]
Mit mapMulti (Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
Vorteil:
mapMulti erstellt keine Zwischen-Streams, sondern „wirft“ Werte direkt in den Ergebnis-Stream.
Visualisierung: flatMap und mapMulti
| Methode | Was gibt die Funktion zurück | Wie wird zusammengeführt | Zwischensammlungen? |
|---|---|---|---|
|
Ein Element | Einfach | Nein |
|
Stream | Verschmilzt | Ja (Zwischen-Streams) |
|
Consumer (0..n-mal) | Verschmilzt | Nein (fügt direkt hinzu) |
6. Typische Fehler beim Arbeiten mit flatMap und mapMulti
Fehler Nr. 1: Stream<Stream<T>> statt Stream<T> erhalten. Oft verwenden Studierende map statt flatMap, wenn sie mit Kollektionen von Kollektionen arbeiten. Dadurch müssen zusätzliche Schleifen geschrieben werden.
Fehler Nr. 2: Falscher Rückgabetyp. Für flatMap muss die Funktion einen Stream zurückgeben, nicht eine List oder ein Array.
Fehler Nr. 3: Ineffizienz. In einfachen Fällen funktioniert flatMap hervorragend, aber wenn für jedes Element Stream.of oder Stream.empty() erzeugt werden muss, kann das überflüssig sein. Für solche Aufgaben ist mapMulti besser geeignet.
Fehler Nr. 4: mapMulti funktioniert in älteren Java-Versionen nicht. mapMulti gibt es erst seit Java 16. Wenn du eine ältere JDK-Version verwendest, ist diese Methode nicht verfügbar.
Fehler Nr. 5: Probleme mit null. Gib aus flatMap niemals null zurück – verwende für „leere“ Fälle immer Stream.empty().
Fehler Nr. 6: Zwischenkollektionen. Erzeuge keine unnötigen Listen oder Streams, wenn du Elemente in mapMulti direkt über den consumer hinzufügen kannst.
GO TO FULL VERSION