CodeGym /Cursos /JAVA 25 SELF /Métodos flatMap y mapMulti

Métodos flatMap y mapMulti

JAVA 25 SELF
Nivel 32 , Lección 0
Disponible

1. Introducción

Imaginemos que tenemos una lista de estudiantes, y cada estudiante tiene una lista de sus aficiones. Por ejemplo:

List<List<String>> hobbies = List.of(
    List.of("Natación", "Ajedrez"),
    List.of("Fútbol"),
    List.of("Programación", "Lectura", "Cine")
);

Tu tarea: obtener una lista única de todas las aficiones, para saber qué interesa en general a los estudiantes. Es lógico intentar usar map:

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

¿Qué obtuvimos? ¡Una lista de flujos! Es decir, Stream<Stream<String>>. No es exactamente lo que necesitamos — querríamos obtener simplemente Stream<String> para trabajar con cada afición directamente.

Imagina que tienes una caja, en la que hay otras cajas con juguetes. El método map simplemente saca cada caja y te enseña la caja (Stream<Stream<String>>). Pero tú querrías ver todos los juguetes de inmediato (Stream<String>), sin tener que hurgar en las cajas.

2. flatMap: cómo «desplegar» colecciones anidadas

Para «desempaquetar las cajas» y obtener un flujo plano, necesitas el método flatMap.
Recibe una función que para cada elemento devuelve un flujo, y «aplana» todos los flujos anidados en uno solo.

Sintaxis de flatMap

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

En pocas palabras: flatMap espera que devuelvas para cada elemento un flujo (Stream), y lo «aplana» todo en un único flujo grande.

Ejemplo: unimos todas las aficiones de los estudiantes

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

System.out.println(allHobbies);
// [Natación, Ajedrez, Fútbol, Programación, Lectura, Cine]

Para cada lista de aficiones llamamos a list.stream() (obtuvimos el flujo de aficiones de un estudiante). Y flatMap «aplanó» todos los flujos en un único flujo grande: ahora vemos todas las aficiones, no flujos de aficiones.

Esquema visual

hobbies.stream()
    |
    |---> [Natación, Ajedrez]    -> stream
    |---> [Fútbol]               -> stream
    |---> [Programación, ...]    -> stream
    |
flatMap: une todo en un solo Stream<String>

¿Por qué map no resuelve el problema?

Si usáramos map, obtendríamos Stream<Stream<String>>, y trabajar con eso es incómodo: por ejemplo, no puedes iterar directamente todas las cadenas; necesitas recorridos adicionales.

3. Ejemplos prácticos de uso de flatMap

Dividir cadenas en caracteres (List<String> → Stream<Character>)

Supongamos que tienes una lista de cadenas y quieres obtener un flujo de todos los caracteres que aparecen en todas las cadenas:

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]

Explicación:

  • word.chars() devuelve un IntStream (flujo de códigos de caracteres).
  • mapToObj convierte los códigos en caracteres.
  • flatMap une todos los flujos de caracteres obtenidos en uno solo.

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

Supongamos que tienes una lista de objetos Optional y quieres obtener un flujo solo de aquellos valores que realmente están presentes:

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]

Truco:
Optional<T> (desde Java 9) tiene el método stream(), que devuelve o bien un flujo vacío, o bien un flujo de un único elemento. flatMap une todos los valores no vacíos en un solo flujo.

4. Nuevo método mapMulti: cuándo flatMap no es necesario

¿Por qué se introdujo mapMulti?

El método mapMulti, introducido en Java 16, funciona de forma similar a flatMap, pero es un poco más eficiente y flexible.

flatMap es una herramienta potente, pero tiene un inconveniente: para cada elemento estás obligado a crear un Stream nuevo, incluso si quieres devolver 0, 1 o varios elementos. Esto puede ser ineficiente, especialmente si solo quieres «desplegar» elementos sin crear colecciones o flujos intermedios.

mapMulti es una versión mejorada de flatMap que permite añadir directamente los valores necesarios al flujo resultante mediante un Consumer, sin crear estructuras intermedias. En lugar de devolver un flujo, indicas directamente qué elementos añadir al flujo resultante a través del Consumer.

Sintaxis de mapMulti

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

Para cada elemento se llama a la función mapper, que recibe el propio elemento y un Consumer en el que se pueden «ir metiendo» nuevos valores.

Ejemplo: filtrado y despliegue de elementos en una sola pasada

Supongamos que tienes una lista de números, y quieres que para los pares se añadan al flujo dos veces y para los impares — ninguna (es decir, filtrado y «despliegue» a la vez):

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); // Añadimos dos veces
        }
        // Para los impares no hacemos nada (filtrado)
    })
    .collect(Collectors.toList());

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

Comparación con flatMap

La misma tarea con flatMap habría que escribirla así:

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

Aquí seguimos obligados a crear Stream.of(...) o Stream.empty() para cada elemento, incluso si no es eficiente.

5. ¿Cuándo usar flatMap y cuándo mapMulti?

  • map: cuando transformas cada elemento en un solo elemento.
  • flatMap: cuando transformas cada elemento en un flujo de elementos.
  • mapMulti: cuando quieres generar varios elementos sin crear un flujo intermedio (más eficiente, especialmente en bucles calientes de rendimiento).

Otro ejemplo: desplegar Map<Integer, List<String>> en Stream<Pair(Integer, String)>

Supongamos que tenemos un mapa: id → lista de aficiones.

Map<Integer, List<String>> studentHobbies = Map.of(
    1, List.of("Natación", "Ajedrez"),
    2, List.of("Fútbol"),
    3, List.of("Programación", "Lectura", "Cine")
);

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

System.out.println(allHobbies);
// [Natación, Ajedrez, Fútbol, Programación, Lectura, Cine]

Y si queremos obtener pares (id, afición):

List<String> pairs = studentHobbies.entrySet().stream()
    .flatMap(entry -> entry.getValue().stream()
        .map(hobby -> entry.getKey() + ": " + hobby))
    .collect(Collectors.toList());

System.out.println(pairs);
// [1: Natación, 1: Ajedrez, 2: Fútbol, 3: Programación, 3: Lectura, 3: Cine]

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

Ventaja:
mapMulti no crea flujos intermedios; simplemente «emite» valores directamente al flujo final.

Visualización: flatMap y mapMulti

Método Qué devuelve la función Cómo combina ¿Colecciones intermedias?
map
Un elemento Simplemente No
flatMap
Flujo (Stream) Concatena Sí (Stream intermedios)
mapMulti
Consumer (0..n veces) Concatena No (añade directamente)

6. Errores típicos al trabajar con flatMap y mapMulti

Error n.º 1: obtuviste Stream<Stream<T>> en lugar de Stream<T>. A menudo los estudiantes usan map en lugar de flatMap cuando trabajan con colecciones de colecciones. Como resultado, hay que escribir bucles extra.

Error n.º 2: tipo de valor de retorno incorrecto. Para flatMap la función debe devolver un Stream, no una List ni un array.

Error n.º 3: ineficiencia. En casos sencillos flatMap funciona muy bien, pero si para cada elemento tienes que crear Stream.of o Stream.empty(), puede ser excesivo. Para esas tareas es mejor usar mapMulti.

Error n.º 4: mapMulti no funciona en versiones antiguas de Java. mapMulti apareció solo en Java 16. Si tienes una versión de JDK más antigua, este método no estará disponible.

Error n.º 5: problemas con null. No devuelvas null desde flatMap — devuelve siempre Stream.empty() para casos «vacíos».

Error n.º 6: colecciones intermedias. No crees listas o flujos de más si puedes añadir elementos directamente mediante el consumer en mapMulti.

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