¿Cómo almacenar claves duplicadas en un mapa en Java?

1. Información general

En este tutorial vamos a explorar las opciones disponibles para manejar un Mapa con claves duplicadas o, en otras palabras, un Mapa que permite almacenar múltiples valores para una sola clave.

2. Mapas estándar

Java tiene varias implementaciones de la interfaz Map , cada una con sus particularidades.

Sin embargo, ninguna de las implementaciones de mapas centrales de Java existentes permiten que un mapa maneje varios valores para una sola clave .

Como podemos ver, si intentamos insertar dos valores para la misma clave, se almacenará el segundo valor, mientras que se eliminará el primero.

También será devuelto (por cada implementación adecuada del método put (tecla K, valor V) ):

Map map = new HashMap(); assertThat(map.put("key1", "value1")).isEqualTo(null); assertThat(map.put("key1", "value2")).isEqualTo("value1"); assertThat(map.get("key1")).isEqualTo("value2"); 

Entonces, ¿cómo podemos lograr el comportamiento deseado?

3. Recaudación como valor

Obviamente, usar una Colección para cada valor de nuestro Mapa haría el trabajo:

Map
    
      map = new HashMap(); List list = new ArrayList(); map.put("key1", list); map.get("key1").add("value1"); map.get("key1").add("value2"); assertThat(map.get("key1").get(0)).isEqualTo("value1"); assertThat(map.get("key1").get(1)).isEqualTo("value2"); 
    

Sin embargo, esta solución detallada tiene múltiples inconvenientes y es propensa a errores. Implica que necesitamos instanciar una Colección para cada valor, verificar su presencia antes de agregar o eliminar un valor, eliminarlo manualmente cuando no quedan valores, etcétera.

Desde Java 8 podríamos aprovechar los métodos compute () y mejorarlo:

Map
    
      map = new HashMap(); map.computeIfAbsent("key1", k -> new ArrayList()).add("value1"); map.computeIfAbsent("key1", k -> new ArrayList()).add("value2"); assertThat(map.get("key1").get(0)).isEqualTo("value1"); assertThat(map.get("key1").get(1)).isEqualTo("value2"); 
    

Si bien esto es algo que vale la pena saber, debemos evitarlo a menos que tengamos una muy buena razón para no hacerlo, como las políticas restrictivas de la empresa que nos impiden usar bibliotecas de terceros.

De lo contrario, antes de escribir nuestra propia implementación personalizada de mapas y reinventar la rueda, deberíamos elegir entre las varias opciones disponibles listas para usar.

4. Colecciones de Apache Commons

Como de costumbre, Apache tiene una solución para nuestro problema.

Comencemos por importar la última versión de Colecciones comunes (CC de ahora en adelante):

 org.apache.commons commons-collections4 4.1 

4.1. MultiMap

El org.apache.commons.collections4. La interfaz MultiMap define un mapa que contiene una colección de valores para cada clave.

Está implementado por org.apache.commons.collections4.map. Clase MultiValueMap , que maneja automáticamente la mayor parte del texto estándar debajo del capó:

MultiMap map = new MultiValueMap(); map.put("key1", "value1"); map.put("key1", "value2"); assertThat((Collection) map.get("key1")) .contains("value1", "value2"); 

Si bien esta clase está disponible desde CC 3.2, no es segura para subprocesos y ha quedado obsoleta en CC 4.1 . Debemos usarlo solo cuando no podamos actualizar a la versión más nueva.

4.2. MultiValuedMap

El sucesor de MultiMap es org.apache.commons.collections4. Interfaz MultiValuedMap . Tiene múltiples implementaciones listas para ser utilizadas.

Veamos cómo almacenar nuestros múltiples valores en una ArrayList , que retiene los duplicados:

MultiValuedMap map = new ArrayListValuedHashMap(); map.put("key1", "value1"); map.put("key1", "value2"); map.put("key1", "value2"); assertThat((Collection) map.get("key1")) .containsExactly("value1", "value2", "value2"); 

Alternativamente, podríamos usar un HashSet , que deja duplicados:

MultiValuedMap map = new HashSetValuedHashMap(); map.put("key1", "value1"); map.put("key1", "value1"); assertThat((Collection) map.get("key1")) .containsExactly("value1"); 

Ambas implementaciones anteriores no son seguras para subprocesos .

Veamos cómo podemos usar el decorador UnmodifiableMultiValuedMap para hacerlos inmutables:

@Test(expected = UnsupportedOperationException.class) public void givenUnmodifiableMultiValuedMap_whenInserting_thenThrowingException() { MultiValuedMap map = new ArrayListValuedHashMap(); map.put("key1", "value1"); map.put("key1", "value2"); MultiValuedMap immutableMap = MultiMapUtils.unmodifiableMultiValuedMap(map); immutableMap.put("key1", "value3"); } 

5. Multimapa de guayaba

Guava es la API de Google Core Libraries para Java.

El com.google.common.collect. La interfaz multimapa está ahí desde la versión 2. En el momento de escribir este artículo, la última versión es la 25, pero como después de la versión 23 se ha dividido en diferentes ramas para jre y android ( 25.0-jre y 25.0-android ), seguiremos usando versión 23 para nuestros ejemplos.

Comencemos importando Guava en nuestro proyecto:

 com.google.guava guava 23.0 

Guava siguió el camino de las múltiples implementaciones desde el principio.

El más común es com.google.common.collect. ArrayListMultimap , que usa un HashMap respaldado por un ArrayList para cada valor:

Multimap map = ArrayListMultimap.create(); map.put("key1", "value2"); map.put("key1", "value1"); assertThat((Collection) map.get("key1")) .containsExactly("value2", "value1"); 

Como siempre, deberíamos preferir las implementaciones inmutables de la interfaz Multimap: com.google.common.collect. ImmutableListMultimap y com.google.common.collect. ImmutableSetMultimap .

5.1. Implementaciones de mapas comunes

Cuando necesitamos una implementación de Map específica , lo primero que debemos hacer es verificar si existe, porque probablemente Guava ya la haya implementado.

For example, we can use the com.google.common.collect.LinkedHashMultimap, which preserves the insertion order of keys and values:

Multimap map = LinkedHashMultimap.create(); map.put("key1", "value3"); map.put("key1", "value1"); map.put("key1", "value2"); assertThat((Collection) map.get("key1")) .containsExactly("value3", "value1", "value2"); 

Alternatively, we can use a com.google.common.collect.TreeMultimap, which iterates keys and values in their natural order:

Multimap map = TreeMultimap.create(); map.put("key1", "value3"); map.put("key1", "value1"); map.put("key1", "value2"); assertThat((Collection) map.get("key1")) .containsExactly("value1", "value2", "value3"); 

5.2. Forging Our Custom MultiMap

Many other implementations are available.

However, we may want to decorate a Map and/or a List not yet implemented.

Luckily, Guava has a factory method allowing us to do it: the Multimap.newMultimap().

6. Conclusion

We've seen how to store multiple values for a key in a Map in all the main existing ways.

Hemos explorado las implementaciones más populares de Apache Commons Collections y Guava, que deberían preferirse a las soluciones personalizadas cuando sea posible.

Como siempre, el código fuente completo está disponible en Github.