CodeGym /Kursy /JAVA 25 SELF /Metody flatMap i mapMulti

Metody flatMap i mapMulti

JAVA 25 SELF
Poziom 32 , Lekcja 0
Dostępny

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?
map
Jeden element Po prostu Nie
flatMap
Strumień (Stream) Scala Tak (pośrednie strumienie)
mapMulti
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.

1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 0
Niedostępne
Zbieranie wszystkich zainteresowań uczniów dla szkolnych kółek 📚
Zbieranie wszystkich zainteresowań uczniów dla szkolnych kółek 📚
1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 0
Niedostępne
Tworzenie katalogu hobby dla każdego studenta 🧑‍🎓
Tworzenie katalogu hobby dla każdego studenta 🧑‍🎓
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION