CodeGym /Corsi /JAVA 25 SELF /I metodi flatMap e mapMulti

I metodi flatMap e mapMulti

JAVA 25 SELF
Livello 32 , Lezione 0
Disponibile

1. Introduzione

Immaginiamo di avere un elenco di studenti, e ogni studente ha un elenco dei propri hobby. Ad esempio:

List<List<String>> hobbies = List.of(
    List.of("Nuoto", "Scacchi"),
    List.of("Calcio"),
    List.of("Programmazione", "Lettura", "Cinema")
);

L’obiettivo è ottenere un elenco unico di tutti gli hobby, per capire cosa interessa agli studenti. È logico provare a usare map:

List<Stream<String>> streams = hobbies.stream()
    .map(list -> list.stream())
    .collect(Collectors.toList());

Cosa abbiamo ottenuto? Un elenco di flussi! Cioè Stream<Stream<String>>. Non è esattamente ciò che serve — vorremmo semplicemente ottenere un Stream<String>, per lavorare direttamente con ogni hobby.

Immagina di avere una scatola in cui ci sono altre scatole con giocattoli. Il metodo map estrae ogni scatola e te la mostra (Stream<Stream<String>>). Ma tu vorresti vedere subito tutti i giocattoli (Stream<String>), senza frugare nelle scatole.

2. flatMap: come «appiattire» le collezioni annidate

Per «spacchettare le scatole» e ottenere un flusso piatto, serve il metodo flatMap.
Accetta una funzione che per ogni elemento restituisce un flusso e «appiattisce» subito tutti i flussi annidati in uno solo.

Sintassi di flatMap

Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

In parole povere: flatMap si aspetta che per ogni elemento restituiate un flusso (Stream), e poi «appiattisce» tutto in un unico grande flusso.

Esempio: uniamo tutti gli hobby degli studenti

List<String> allHobbies = hobbies.stream()
    .flatMap(list -> list.stream())
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [Nuoto, Scacchi, Calcio, Programmazione, Lettura, Cinema]

Per ogni elenco di hobby abbiamo chiamato list.stream() (ottenendo il flusso degli hobby di un singolo studente). E flatMap ha «spianato» tutti i flussi in un unico grande flusso — ora vediamo tutti gli hobby, non i flussi di hobby.

Schema visivo

hobbies.stream()
    |
    |---> [Nuoto, Scacchi]       -> stream
    |---> [Calcio]               -> stream
    |---> [Programmazione, ...]  -> stream
    |
flatMap: unisce tutto in un unico Stream<String>

Perché map non risolve il problema?

Se usassimo map, otterremmo Stream<Stream<String>>, e lavorarci non è comodo: ad esempio, non si possono scorrere subito tutte le stringhe; servono passaggi aggiuntivi.

3. Esempi pratici di utilizzo di flatMap

Scomporre le stringhe in caratteri (List<String> → Stream<Character>)

Supponiamo di avere un elenco di stringhe e di voler ottenere un flusso di tutti i caratteri presenti in tutte le stringhe:

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]

Spiegazione:

  • word.chars() restituisce un IntStream (flusso di codici dei caratteri).
  • mapToObj trasforma i codici in caratteri.
  • flatMap unisce tutti i flussi di caratteri ottenuti in uno solo.

Lavorare con Optional: Stream<Optional<T>> → Stream<T>

Supponiamo di avere un elenco di oggetti Optional e di voler ottenere un flusso solo dei valori effettivamente presenti:

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]

Nota:
Optional<T> (a partire da Java 9) ha il metodo stream(), che restituisce un flusso vuoto oppure un flusso di un solo elemento. flatMap unisce tutti i valori non vuoti in un unico flusso.

4. Il nuovo metodo mapMulti: quando flatMap non serve

Perché è nato mapMulti?

Introdotto in Java 16, mapMulti funziona in modo simile a flatMap, ma è un po’ più efficiente e flessibile.

flatMap è uno strumento potente, ma ha uno svantaggio: per ogni elemento sei obbligato a creare un nuovo Stream, anche se vuoi restituire 0, 1 o più elementi. Questo può essere inefficiente, specialmente se vuoi solo «espandere» gli elementi senza creare collezioni o flussi intermedi.

mapMulti è una versione migliorata di flatMap che permette di aggiungere direttamente i valori necessari al flusso risultante tramite un Consumer, senza creare strutture intermedie. Invece di restituire un flusso, indichi direttamente quali elementi aggiungere al flusso di output tramite un Consumer.

Sintassi di mapMulti

<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)

Per ogni elemento viene chiamata la funzione mapper, che riceve l’elemento stesso e un Consumer nel quale è possibile «accumulare» i nuovi valori.

Esempio: filtraggio ed espansione degli elementi in un solo passaggio

Supponiamo di avere un elenco di numeri e, per i numeri pari, di volerli aggiungere al flusso due volte, mentre per i dispari — mai (cioè filtraggio ed «espansione» insieme):

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); // Aggiungiamo due volte
        }
        // Per i dispari non facciamo nulla (filtraggio)
    })
    .collect(Collectors.toList());

System.out.println(result);
// [2, 2, 4, 4]

Confronto con flatMap

La stessa cosa con flatMap andrebbe scritta così:

List<Integer> result = numbers.stream()
    .flatMap(number -> number % 2 == 0
        ? Stream.of(number, number)
        : Stream.empty())
    .collect(Collectors.toList());

Qui siamo comunque costretti a creare Stream.of(...) o Stream.empty() per ogni elemento, anche quando non è efficiente.

5. Quando usare flatMap e quando mapMulti?

  • map — quando trasformi ogni elemento in un singolo elemento.
  • flatMap — quando trasformi ogni elemento in un flusso di elementi.
  • mapMulti — quando vuoi generare più elementi senza creare un flusso intermedio (più efficiente, soprattutto nei cicli caldi).

Altro esempio: espandere Map<Integer, List<String>> in Stream<Pair(Integer, String)>

Supponiamo di avere una mappa: id → elenco di hobby.

Map<Integer, List<String>> studentHobbies = Map.of(
    1, List.of("Nuoto", "Scacchi"),
    2, List.of("Calcio"),
    3, List.of("Programmazione", "Lettura", "Cinema")
);

List<String> allHobbies = studentHobbies.values().stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [Nuoto, Scacchi, Calcio, Programmazione, Lettura, Cinema]

E se volessimo ottenere le coppie (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: Nuoto, 1: Scacchi, 2: Calcio, 3: Programmazione, 3: Lettura, 3: Cinema]

Con 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());

Vantaggio:
mapMulti non crea flussi intermedi, ma «emette» direttamente i valori nel flusso finale.

Visualizzazione: flatMap e mapMulti

Metodo Cosa restituisce la funzione Come combina Collezioni intermedie?
map
Un elemento Direttamente No
flatMap
Flusso (Stream) Concatena Sì (Stream intermedi)
mapMulti
Consumer (0..n volte) Concatena No (aggiunge direttamente)

6. Errori tipici nell’uso di flatMap e mapMulti

Errore n. 1: avete ottenuto Stream<Stream<T>> invece di Stream<T>. Spesso gli studenti usano map invece di flatMap quando lavorano con collezioni di collezioni. Di conseguenza, si finisce per scrivere cicli superflui.

Errore n. 2: tipo di valore restituito errato. Per flatMap la funzione deve restituire uno Stream, non una List o un array.

Errore n. 3: inefficienza. Nei casi semplici flatMap funziona benissimo, ma se per ogni elemento devi creare Stream.of o Stream.empty(), può essere ridondante. Per questi scenari è meglio usare mapMulti.

Errore n. 4: mapMulti non funziona nelle vecchie versioni di Java. mapMulti è apparso solo con Java 16. Se hai una versione JDK più vecchia, questo metodo non sarà disponibile.

Errore n. 5: problemi con null. Non restituire null da flatMap — restituisci sempre Stream.empty() per i casi «vuoti».

Errore n. 6: collezioni intermedie. Non creare elenchi o flussi superflui se puoi aggiungere gli elementi direttamente tramite il consumer in mapMulti.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION