1. Wprowadzenie
Wyobraźmy sobie, że mamy listę studentów, a każdy student ma listę swoich zainteresowań. Na przykład:
List<List<String>> hobbies = List.of(
List.of("Pływanie", "Szachy"),
List.of("Piłka nożna"),
List.of("Programowanie", "Czytanie", "Kino")
);
Twoje zadanie: uzyskać jednolitą listę wszystkich zainteresowań, aby dowiedzieć się, czym w ogóle interesują się studenci. Logiczne wydaje się użycie map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
Co otrzymaliśmy? Listę strumieni! Czyli Stream<Stream<String>>. To nie do końca to, czego chcemy — wolelibyśmy dostać po prostu Stream<String>, aby pracować bezpośrednio z każdym zainteresowaniem.
Wyobraź sobie, że masz pudełko, w którym leżą inne pudełka z zabawkami. Metoda map po prostu wyjmuje każde pudełko i pokazuje ci pudełko (Stream<Stream<String>>). A ty chciałbyś od razu widzieć wszystkie zabawki (Stream<String>), bez grzebania w pudełkach.
2. flatMap: jak „rozwinąć” zagnieżdżone kolekcje
Aby „rozpakować pudełka” i uzyskać płaski strumień, potrzebna jest metoda flatMap.
Przyjmuje ona funkcję, która dla każdego elementu zwraca strumień, i od razu „spłaszcza” wszystkie zagnieżdżone strumienie do jednego.
Składnia flatMap
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Mówiąc prościej: flatMap oczekuje, że dla każdego elementu zwrócisz strumień (Stream), a on to wszystko „wygładzi” do jednego dużego strumienia.
Przykład: łączymy wszystkie zainteresowania studentów
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Pływanie, Szachy, Piłka nożna, Programowanie, Czytanie, Kino]
Dla każdej listy zainteresowań wywołaliśmy list.stream() (otrzymaliśmy strumień zainteresowań jednego studenta). A flatMap „wygładził” wszystkie strumienie do jednego dużego strumienia — teraz widzimy wszystkie zainteresowania, a nie strumienie zainteresowań.
Schemat wizualny
hobbies.stream()
|
|---> [Pływanie, Szachy] -> stream
|---> [Piłka nożna] -> stream
|---> [Programowanie, ...] -> stream
|
flatMap: łączy wszystko w jeden Stream<String>
Dlaczego map nie rozwiązuje zadania?
Gdybyśmy użyli map, otrzymalibyśmy Stream<Stream<String>>, i praca z tym jest niewygodna: na przykład nie można od razu przeiterować wszystkich napisów, trzeba wykonywać dodatkowe przebiegi.
3. Praktyczne przykłady użycia flatMap
Podział napisów na znaki (List<String> → Stream<Character>)
Załóżmy, że masz listę napisów i chcesz uzyskać strumień wszystkich znaków występujących we wszystkich napisach:
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]
Wyjaśnienie:
- word.chars() zwraca IntStream (strumień kodów znaków).
- mapToObj zamienia kody na znaki.
- flatMap łączy wszystkie powstałe strumienie znaków w jeden.
Praca z Optional: Stream<Optional<T>> → Stream<T>
Załóżmy, że masz listę obiektów typu Optional i chcesz uzyskać strumień tylko tych wartości, które rzeczywiście są obecne:
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]
Ważna rzecz:
Optional<T> (począwszy od Java 9) ma metodę stream(), która zwraca albo pusty strumień, albo strumień z jednym elementem. flatMap łączy wszystkie niepuste wartości w jeden strumień.
4. Nowa metoda mapMulti: kiedy flatMap nie jest potrzebny
Dlaczego pojawił się mapMulti?
Wprowadzona w Java 16 metoda mapMulti działa podobnie do flatMap, ale jest nieco wydajniejsza i bardziej elastyczna.
flatMap to potężne narzędzie, ale ma wadę: dla każdego elementu musisz utworzyć nowy Stream, nawet jeśli chcesz zwrócić 0, 1 lub kilka elementów. Może to być nieefektywne, zwłaszcza gdy po prostu chcesz „rozwinąć” elementy bez tworzenia pośrednich kolekcji lub strumieni.
mapMulti to ulepszona wersja flatMap, która pozwala bezpośrednio dodawać potrzebne wartości do wynikowego strumienia przez Consumer, bez tworzenia pośrednich struktur. Zamiast zwracać strumień, bezpośrednio wskazujesz, jakie elementy dodać do wynikowego strumienia, używając Consumer.
Składnia mapMulti
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
Dla każdego elementu wywoływana jest funkcja mapper, która otrzymuje sam element oraz Consumer, do którego można „wkładać” nowe wartości.
Przykład: filtrowanie i rozwijanie elementów w jednym przebiegu
Załóżmy, że masz listę liczb i chcesz dla liczb parzystych dodać je do strumienia dwukrotnie, a dla nieparzystych — ani razu (czyli filtrowanie i „rozwijanie” jednocześnie):
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); // Dodajemy dwa razy
}
// Dla nieparzystych nic nie robimy (filtrowanie)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Porównanie z flatMap
To samo zadanie przez flatMap trzeba by zapisać tak:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Tutaj i tak jesteśmy zmuszeni tworzyć Stream.of(...) lub Stream.empty() dla każdego elementu, nawet jeśli jest to nieefektywne.
5. Kiedy używać flatMap, a kiedy mapMulti?
- map — gdy przekształcasz każdy element w jeden element.
- flatMap — gdy przekształcasz każdy element w strumień elementów.
- mapMulti — gdy chcesz wygenerować kilka elementów bez tworzenia pośredniego strumienia (wydajniej, zwłaszcza w gorących pętlach).
Jeszcze jeden przykład: rozwinięcie Map<Integer, List<String>> do Stream<Pair(Integer, String)>
Załóżmy, że mamy mapę: id → lista hobby.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Pływanie", "Szachy"),
2, List.of("Piłka nożna"),
3, List.of("Programowanie", "Czytanie", "Kino")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Pływanie, Szachy, Piłka nożna, Programowanie, Czytanie, Kino]
A jeśli chcemy otrzymać pary (id, hobby):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Pływanie, 1: Szachy, 2: Piłka nożna, 3: Programowanie, 3: Czytanie, 3: Kino]
Za pomocą 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());
Zaleta:
mapMulti nie tworzy pośrednich strumieni, tylko bezpośrednio „wyrzuca” wartości do wynikowego strumienia.
Wizualizacja: flatMap i mapMulti
| Metoda | Co zwraca funkcja | Jak łączy | Kolekcje pośrednie? |
|---|---|---|---|
|
Jeden element | Po prostu | Nie |
|
Strumień (Stream) | Scala | Tak (pośrednie strumienie) |
|
Consumer (0..n razy) | Scala | Nie (dodaje bezpośrednio) |
6. Typowe błędy podczas pracy z flatMap i mapMulti
Błąd nr 1: Otrzymany Stream<Stream<T>> zamiast Stream<T>. Studenci często używają map zamiast flatMap, gdy pracują z kolekcjami kolekcji. W rezultacie trzeba pisać zbędne pętle.
Błąd nr 2: Nieprawidłowy typ wartości zwracanej. Dla flatMap funkcja musi zwracać Stream, a nie List ani tablicę.
Błąd nr 3: Niewydajność. W prostych przypadkach flatMap działa świetnie, ale jeśli dla każdego elementu trzeba tworzyć Stream.of lub Stream.empty(), może to być przerost formy nad treścią. Do takich zadań lepiej użyć mapMulti.
Błąd nr 4: mapMulti nie działa w starszych wersjach Java. mapMulti pojawiło się dopiero w Java 16. Jeśli masz starszą wersję JDK, ta metoda będzie niedostępna.
Błąd nr 5: Problemy z null. Nie zwracaj null z flatMap — zawsze zwracaj Stream.empty() dla „pustych” przypadków.
Błąd nr 6: Kolekcje pośrednie. Nie twórz zbędnych list ani strumieni, jeśli można dodać elementy bezpośrednio przez consumer w mapMulti.
GO TO FULL VERSION