Tutorial Completo para Fusionar Mapas en Java 8

Cómo Fusionar Dos Mapas en Java 8: Un Tutorial Completo



1. Introducción

En este tutorial rápido, demostraremos cómo fusionar dos mapas utilizando las capacidades de Java 8. Para ser más específicos, examinaremos diferentes escenarios de fusión, incluyendo mapas que tienen entradas duplicadas. La capacidad de fusionar estructuras de datos de manera eficiente es esencial para muchos programadores que trabajan en aplicaciones complejas donde los datos provienen de múltiples fuentes.



2. Inicialización

Para empezar, definiremos dos instancias de Map:

private static Map<String, Employee> map1 = new HashMap<>();
private static Map<String, Employee> map2 = new HashMap<>&

La clase Employee luce de la siguiente manera:

public class Employee {
    private Long id;
    private String name;

    // constructor, getters, setters
}

Ahora podemos insertar algunos datos en las instancias de Map:

Employee employee1 = new Employee(1L, "Henry");
map1.put(employee1.getName(), employee1);
Employee employee2 = new Employee(22L, "Annie");
map1.put(employee2.getName(), employee2);
Employee employee3 = new Employee(8L, "John");
map1.put(employee3.getName(), employee3);

Employee employee4 = new Employee(2L, "George");
map2.put(employee4.getName(), employee4);
Employee employee5 = new Employee(3L, "Henry");
map2.put(employee5.getName(), employee5);

Cabe destacar que tenemos claves idénticas para las entradas employee1 y employee5, lo cual utilizaremos más adelante.



3. Map.merge()

Java 8 agrega una nueva función merge() en la interfaz java.util.Map. La función merge() funciona de la siguiente manera; si la clave especificada no está asociada a un valor o el valor es nulo, asocia la clave con el valor dado. De lo contrario, reemplaza el valor con los resultados de la función de remapeo dada. Si el resultado de la función de remapeo es nulo, elimina la entrada resultante.

Primero, construiremos un nuevo HashMap copiando todas las entradas de map1:

Map<String, Employee> map3 = new HashMap<>(map1);

A continuación, introduciremos la función merge(), junto con una regla de fusión:

map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(), v2.getName()));

Finalmente, iteraremos sobre map2 y fusionaremos las entradas en map3:

map2.forEach(
    (key, value) -> map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(), v2.getName())));

Ejecutemos el programa y imprimamos el contenido de map3:

John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
George=Employee{id=2, name='George'}
Henry=Employee{id=1, name='Henry'}

Como resultado, nuestro mapa combinado tiene todos los elementos de las entradas del HashMap anterior. Las entradas con claves duplicadas han sido fusionadas en una sola entrada. También podemos ver que el objeto Employee de la última entrada tiene el id de map1, y el valor es tomado de map2.

Esta es una construcción poderosa, permitiendo personalizar cómo se manejan las fusiones dependiendo de las necesidades de la aplicación.



4. Stream.concat()

La API de Stream en Java 8 también puede proporcionar una solución fácil a nuestro problema. Primero, necesitamos combinar nuestras instancias de Map en un solo Stream. Esto es exactamente lo que la operación Stream.concat() hace:

Stream combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream());

Aquí pasamos los conjuntos de entradas del mapa como parámetros.

A continuación, necesitamos recopilar nuestro resultado en un nuevo Map. Para eso, podemos usar Collectors.toMap():

Map<String, Employee> result = combined.collect(
    Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Como resultado, el recolector usará las claves y valores existentes de nuestros mapas. Pero esta solución está lejos de ser perfecta. Tan pronto como nuestro recolector encuentre entradas con claves duplicadas, lanzará una IllegalStateException.

Para manejar este problema, podemos simplemente agregar un tercer parámetro lambda de “fusión” en nuestro recolector:

(value1, value2) -> new Employee(value2.getId(), value1.getName());

Esto usará la expresión lambda cada vez que se detecte una clave duplicada.

Finalmente, reunimos todo:

Map<String, Employee> result = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey, 
        Map.Entry::getValue,
        (value1, value2) -> new Employee(value2.getId(), value1.getName())));

Ahora ejecutemos el código y veamos los resultados:

George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=3, name='Henry'}

Como podemos ver, las entradas duplicadas con la clave “Henry” fueron fusionadas en un nuevo par clave-valor, donde el id del nuevo Employee fue tomado de map2 y el valor de map1.



5. Stream.of()

Para seguir utilizando la API de Stream, podemos convertir nuestras instancias de Map en un flujo unificado con la ayuda de Stream.of(). Aquí no tenemos que crear una colección adicional para trabajar con los flujos:

Map<String, Employee> map3 = Stream.of(map1, map2)
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (v1, v2) -> new Employee(v1.getId(), v2.getName()));

Primero, transformamos map1 y map2 en un solo flujo. Luego, convertimos el flujo en un mapa. Como podemos ver, el último argumento de toMap() es una función de fusión. Esto resuelve el problema de las claves duplicadas al seleccionar el campo id de la entrada v1, y el nombre de v2.

Aquí está el impreso de la instancia map3 después de ejecutar el programa:

George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=1, name='Henry'}


6. Fusión Simplificada

Adicionalmente, podemos usar un pipeline de stream() para ensamblar nuestras entradas del mapa. El siguiente fragmento de código demuestra cómo agregar las entradas de map2 y map1 ignorando las entradas duplicadas:

Map<String, Employee> map3 = map2.entrySet()
    .stream()
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (v1, v2) -> new Employee(v1.getId(), v2.getName()),
        () -> new HashMap<>(map1)));

Como esperábamos, los resultados después de la fusión son:

{John=Employee{id=8, name='John'},
 Annie=Employee{id=22, name='Annie'},
 George=Employee{id=2, name='George'},
 Henry=Employee{id=1, name='Henry'}}


7. StreamEx

Además de las soluciones proporcionadas por el JDK, también podemos usar la popular biblioteca StreamEx. En términos sencillos, [StreamEx] es una mejora para la API de Stream, y proporciona muchos métodos adicionales útiles. Usaremos una instancia de EntryStream para operar sobre pares clave-valor:

Map<String, Employee> map3 = EntryStream.of(map1)
    .append(EntryStream.of(map2))
    .toMap((e1, e2) -> e1);

La idea es fusionar los flujos de nuestros mapas en uno solo. Luego recopilaremos las entradas en la nueva instancia map3. También es importante mencionar la expresión (e1, e2) -> e1, ya que ayuda a definir la regla para lidiar con las claves duplicadas. Sin esto, nuestro código lanzará una IllegalStateException.

Y ahora, los resultados:

{George=Employee{id=2, name='George'},
 John=Employee{id=8, name='John'},
 Annie=Employee{id=22, name='Annie'},
 Henry=Employee{id=1, name='Henry'}}


8. Resumen

En este breve artículo, hemos aprendido diferentes maneras de fusionar mapas en Java 8. Más específicamente, hemos utilizado Map.merge(), la API de Stream, y la biblioteca StreamEx. La capacidad de combinar mapas de manera eficiente es solo uno de los muchos beneficios que Java 8 ha traído a los programadores, haciéndolo más fácil y rápido manipular colecciones.

Al finalizar, es importante recordar que al fusionar mapas, especialmente cuando se enfrentan a claves duplicadas, es crucial definir claramente cómo queremos manejar esos casos en función de la lógica y las necesidades de nuestra aplicación. Si lo hacemos correctamente, la fusión de datos puede ser una tarea liviana y agradable.

Si eres un programador apasionado por Java, te animo a explorar más sobre estas funcionalidades y experimentar con ejemplos en tu entorno de desarrollo. Esto no solo aumentará tu comprensión del lenguaje, sino que también mejorará tu habilidad para gestionar datos eficientemente.