Java 8 - Diferencia entre map() y flatMap()

En esta guía, aprenda cuál es la diferencia entre los métodos map() y flatMap() de Java 8, en el contexto de Optionals y Stream API, con ejemplos prácticos de código y casos de uso.

Introducción

Si bien Java es principalmente un lenguaje orientado a objetos, muchos conceptos de programación funcional se han incorporado al lenguaje. La programación funcional usa funciones para crear y componer lógica de programación, generalmente de manera declarativa (es decir, diciéndole al programa lo que quiere y no cómo hacerlo).

Si desea obtener más información sobre las interfaces funcionales y una visión holística de la programación funcional en Java, lea nuestra [Guía de interfaces funcionales y expresiones lambda en Java](/guía-de-interfaces-funcionales-y-lambda- expresiones-lambda-en-java/)!

Con la introducción de JDK 8, Java agregó una serie de construcciones clave de programación funcional, incluidas map() y flatMap().

{.icon aria-hidden=“true”}

Nota: Esta guía cubre estas dos funciones en el contexto de sus diferencias.

La función map() se utiliza para transformar un flujo de una forma a otra, mientras que la función flatMap() es una combinación de operaciones de mapa y aplanamiento.

Si desea leer más sobre estas funciones individualmente con detalles detallados, puntos de referencia de eficiencia, casos de uso y mejores prácticas, lea nuestro [Java 8 Streams: The Definitive Guide to flatMap()](/java- 8 -transmit-the-flat-map-definitive-guide/) y Java 8 - Ejemplos de Stream.map()!

¡Comencemos destacando primero sus diferencias en Opcionales!

Diferencia entre map() y flatMap() en Opcionales {#difference betweenmapandflatmapinOptionals}

Para comprender la diferencia entre map() y flatMap() en Opcionales, primero debemos comprender brevemente el concepto de Opcionales. La clase opcional se introdujo en Java 8 para presentar la forma más fácil de tratar con NullPointerException.

Según la [documentación] oficial (https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html):

Opcional es un objeto contenedor que puede contener o no un valor no nulo.

La clase opcional sirve para representar si un valor está presente o no. La clase Optional tiene una amplia gama de métodos que se agrupan en dos categorías:

  1. Métodos de Creación: Estos métodos se encargan de crear objetos Opcionales de acuerdo al caso de uso.
  2. Métodos de instancia: Estos métodos operan en un objeto Opcional existente, determinan si el valor está presente o no, recuperan el objeto contenedor, lo manipulan y finalmente devuelven el objeto Opcional actualizado.

map() y flatMap() se pueden usar con la clase Optional y, dado que se usaban con frecuencia para envolver y desenvolver opcionales anidados, también se agregaron métodos en la clase misma.

La firma de la función map() en Opcional es:

1
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)

La firma del flatMap() en Opcional es:

1
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)

Las funciones map() y flatMap() toman las funciones del mapeador como argumentos y generan un Optional<U>. La distinción entre estos dos se nota cuando la función map() se usa para transformar su entrada en valores Opcionales. La función map() envolvería los valores Opcionales existentes con otro Opcional, mientras que la función flatMap() aplana la estructura de datos para que los valores mantengan solo un envoltorio Opcional.

Tratemos de entender el problema con el siguiente código:

1
2
3
Optional optionalObj1 = Optional.of("STACK ABUSE")
  .map(s -> Optional.of("STACK ABUSE"));
System.out.println(optionalObj1);

El siguiente es el resultado de lo anterior:

1
Optional[Optional[STACK ABUSE]]

Como podemos ver, la salida de map() se ha envuelto en un Opcional adicional. Por otro lado, al usar un flatMap() en lugar de un map():

1
2
3
Optional optionalObj2 = Optional.of("STACK ABUSE")
  .flatMap(s -> Optional.of("STACK ABUSE"));
System.out.println(optionalObj2);

Terminamos con:

1
Optional[STACK ABUSE]

flatMap() no vuelve a envolver el resultado en otro Opcional, por lo que nos quedamos con el original. Este mismo comportamiento se puede usar para desenvolver opcionales.

Dado que los ejemplos simples como el que hemos cubierto ahora no transmiten perfectamente cuándo este mecanismo realmente hace o deshace una función, creemos un entorno pequeño en el que lo haga. El siguiente ejemplo muestra un Sistema de Gestión de Investigación, que además, realiza un seguimiento de los investigadores en un instituto.

Dado un servicio simulado que obtiene un investigador en función de algún researcherId, no se garantiza que obtengamos un resultado, por lo que cada Investigador se envuelve como opcional. Además, su StudyArea podría no estar presente por alguna razón (como un área que aún no se ha asignado si un investigador es nuevo en el instituto), por lo que también es un valor opcional.

Dicho esto, si tuviera que buscar a un investigador y obtener su área de estudio, haría algo como lo siguiente:

1
2
3
4
5
6
7
8
9
Optional<Researcher> researcherOptional = researcherService.findById(researcherId);

Optional<StudyArea> studyAreaOptional = researcherOptional
    .map(res -> Researcher.getResearchersStudyArea(res.getId()))
    .filter(studyArea -> studyArea.getTopic().equalsIgnoreCase("Machine Learning"));

System.out.println(studyAreaOptional.isPresent());
System.out.println(studyAreaOptional);
System.out.println(studyAreaOptional.get().getTopic());

Veamos el resultado de este código:

1
2
3
true 
Optional[[correo electrónico protegido]] 
Machine Learning

Porque StudyArea, que es un valor opcional depende de otro valor opcional, está envuelto como un doble opcional en el resultado. Esto no funciona muy bien para nosotros, ya que tendríamos que obtener() el valor una y otra vez. Además, incluso si StudyArea fuera de hecho, null, la comprobación isPresent() devolvería true.

Un opcional de un opcional vacío, no está vacío en sí mismo.

1
2
3
4
5
Optional optional1 = Optional.empty();
Optional optional2 = Optional.of(optional1);

System.out.println(optional2.isPresent());
// true

En este escenario, isPresent() verifica algo que realmente no queremos verificar, la segunda línea realmente no muestra el StudyArea que queremos ver y la línea final arrojará una NullPointerException si StudyArea no está realmente presente. Aquí - map() hace bastante daño porque:

  • El mapa devuelve un opcional vacío si el objeto Investigador está ausente en el objeto ‘opcionalInvestigador’.
  • El mapa devuelve un opcional vacío si getResearchersStudyArea devuelve un valor nulo en lugar del objeto StudyArea.

Alternativamente, puede visualizar la canalización:

Diagrama de flujo de datos de Researcher transformados con las funciones de filtro y mapa

La declaración opcionalInvestigador.map(res -> Investigador.getResearchersStudyArea(res.getId()) ahora producirá un objeto Opcional<Opcional<Investigador>>. Podemos resolver este problema usando flatMap() como no envolverá el resultado en otro Opcional:

1
2
3
Optional<StudyArea> studyAreaOptional = optionalResearcher
        .flatMap(res -> Researcher.getResearchersStudyArea(res.getId()))
        .filter(studyArea -> studyArea.getTopic().equalsIgnoreCase("Machine Learning"));

Diagrama de flujo de datos de Researcher transformados con flatMap y funciones de filtro

De esta manera, ¡las tres líneas que hemos usado para mostrar información sobre el trabajo del investigador según lo previsto!

Diferencia entre map() y flatMap() en Streams {#difference betweenmapandflatmapinstreams}

Para entender la diferencia entre map() y flatMap() en Streams, vale la pena recordarnos cómo funcionan los Streams. La Streams API se introdujo en Java 8 y ha demostrado ser una herramienta extremadamente poderosa para trabajar con colecciones de objetos. Un flujo se puede caracterizar como una secuencia de datos, que proviene de una fuente, en la que se pueden conectar numerosos procedimientos/transformaciones diferentes para producir el resultado deseado.

Hay tres etapas en la tubería de flujo:

  1. Fuente: Denota el origen de un arroyo.
  2. Operaciones intermedias: Estos son los procesos intermedios que cambian los flujos de una forma a otra, como su nombre lo indica. El procesamiento de flujo puede tener cero o varios procesos intermedios.
  3. Operaciones de terminal: Este es el último paso en el proceso que da como resultado un estado final que es el resultado final de la canalización. La operación de terminal más común es recopilar el flujo de nuevo en una ‘Colección’ tangible. Sin esta etapa, el resultado sería imposible de obtener.

map() y flaMap() son las operaciones intermedias que ofrece Stream en el paquete java.util.stream.Stream.

La firma del map() es:

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

La firma del flatMap() es:

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

Como se puede ver en las firmas del método, tanto map() como flatMap() toman funciones de mapeo como argumentos y devuelven un Stream<R> como salida. La única diferencia en los argumentos es que map() toma un Stream<T> como entrada mientras que flatMap() toma un Stream<Stream<T>> como entrada.

{.icon aria-hidden=“true”}

En resumen, map() acepta un Stream<T> y asigna sus elementos a Stream<R>, donde cada R resultante tiene una T inicial correspondiente, mientras que flatMap() acepta un Stream<Stream<T>> y mapea el elemento de cada sub-flujo en un nuevo Stream<R> que representa una lista aplanada de flujos originales.

Además, map() y flatMap() se pueden distinguir de manera que map() genera un solo valor contra una entrada mientras que flatMap() genera cero o cualquier valor numérico contra una entrada. En otras palabras, map() se usa para transformar los datos, mientras que flatMap() se usa para transformar y aplanar la transmisión.

El siguiente es un ejemplo de mapeo uno a uno en map():

1
2
3
4
5
List<String> websiteNamesList = Stream.of("Stack", "Abuse")
            .map(String::toUpperCase)
            .collect(Collectors.toList());

System.out.println(websiteNamesList);

Esto resulta en:

1
[STACK, ABUSE]

Hemos mapeado los valores originales a sus contrapartes en mayúsculas: fue un proceso transformador donde un Stream<T> se asignó a Stream<R>.

Por otro lado, si estuviéramos trabajando con Streams más complejos:

1
2
3
4
5
6
7
8
9
Stream<String> stream1 = Stream.of("Stack", "Abuse");
Stream<String> stream2 = Stream.of("Real", "Python");
Stream<Stream<String>> stream = Stream.of(stream1, stream2);

List<String> namesFlattened = stream
        .flatMap(s -> s)
        .collect(Collectors.toList());

System.out.println(namesFlattened);

Aquí, tenemos un flujo de flujos, donde cada flujo contiene un par de elementos. Cuando flatmapping, estamos tratando con streams, no con elementos. Aquí, hemos decidido dejar los flujos como están (sin ejecutar operaciones en ellos) a través de s->s, y recopilamos sus elementos en una lista. flatMap() recopila los elementos de las subtransmisiones en una lista, no las transmisiones en sí, por lo que terminamos con:

1
[Stack, Abuse, Real, Python]

Un ejemplo más ilustrativo podría basarse en el Sistema de gestión de la investigación. Digamos que queremos agrupar los datos de los investigadores en categorías según sus áreas de estudio en un mapa Map<String, List<Researcher>> donde la clave es un área de estudio y la lista corresponde a las personas que trabajan en ella. Tendríamos una lista de investigadores con los que trabajar antes de agruparlos, naturalmente.

En este conjunto de entradas, es posible que deseemos filtrar o realizar otras operaciones en los propios investigadores. En la mayoría de los casos, map() no funcionará o se comportará de forma extraña porque no podemos aplicar muchos métodos, como filter(), directamente a Map<String, List<Researcher>>. Esto nos lleva al uso de flatMap(), donde transmitimos() cada lista y luego realizamos operaciones en esos elementos.

Con el escenario anterior en mente, considere el siguiente ejemplo, que demuestra el mapeo uno-a-muchos de flatMap():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ResearchService researchService = new ResearchService();
Map<String, List<Researcher>> researchMap = new HashMap<>();
List<Researcher> researcherList = researchService.findAll();

researchMap.put("Machine Learning", researcherList);

List<Researcher> researcherNamesList = researchMap.entrySet().stream()
        // Stream each value in the map's entryset (list of researchers)
        .flatMap(researchers -> researchers.getValue().stream())
        // Arbitrary filter for names starting with "R"
        .filter(researcher -> researcher.getName().startsWith("R"))
        // Collect Researcher objects to list
        .collect(Collectors.toList());

researcherNamesList.forEach(researcher -> {
    System.out.println(researcher.getName());
});

La clase ‘Investigador’ solo tiene un ‘id’, ’nombre’ y ‘dirección de correo electrónico’:

1
2
3
4
5
6
7
public class Researcher {
    private int id;
    private String name;
    private String emailAddress;

    // Constructor, getters and setters 
}

Y ResearchService es un servicio simulado que pretende llamar a una base de datos, devolviendo una lista de objetos. Podemos simular fácilmente el servicio devolviendo una lista codificada (o generada) en su lugar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class ResearchService {

    public List<Researcher> findAll() {
        Researcher researcher1 = new Researcher();
        researcher1.setId(1);
        researcher1.setEmailAddress("[correo electrónico protegido]");
        researcher1.setName("Reham Muzzamil");

        Researcher researcher2 = new Researcher();
        researcher2.setId(2);
        researcher2.setEmailAddress("[correo electrónico protegido]");
        researcher2.setName("John Doe");
        
        // Researcher researcherN = new Researcher();
        // ...
        
        return Arrays.asList(researcher1, researcher2);
    }
}

Si ejecutamos el fragmento de código, aunque solo haya una lista en el mapa, todo el mapa se aplanó a una lista de investigadores, se filtró con un filtro y el único investigador que quedó es:

1
Reham Muzzamil

Si visualizamos la canalización, se vería así:

Flujo de datos del uso de la función flatMap() para filtrar investigadores cuyo nombre comienza con \"R\"

Si reemplazáramos flatMap() con map():

1
.map(researchers -> researchers.getValue().stream()) // Stream<Stream<Researcher>>

No podríamos continuar con el filtro(), ya que estaríamos trabajando con un flujo anidado. En su lugar, aplanamos el flujo de flujos en uno solo y luego ejecutamos operaciones en estos elementos.

Conclusión

En esta guía, hemos visto la diferencia entre map() y flatMap() en Optional y Stream junto con sus casos de uso y ejemplos de código.

En resumen, en el contexto de la clase ‘Opcional’, tanto ‘map()’ como ‘flatMap()’ se usan para transformar ‘Opcional’ en ‘Opcional’, pero si la función de mapeo genera un valor opcional, map() agrega una capa adicional, mientras que flatMap() funciona sin problemas con opciones anidadas y devuelve el resultado en una sola capa de valores opcionales.

De manera similar, map() y flatMap() también se pueden aplicar a Streams - donde map() toma un Stream<T> y devuelve un Stream<R> donde T los valores se asignan a R, mientras que flatMap() toma un Stream<Stream<T>> y devuelve un Stream<R>.