Comment stocker des clés en double dans une carte en Java?

1. Vue d'ensemble

Dans ce tutoriel, nous allons explorer les options disponibles pour gérer une carte avec des clés en double ou, en d'autres termes, une carte qui permet de stocker plusieurs valeurs pour une seule clé.

2. Cartes standard

Java a plusieurs implémentations de l'interface Map , chacune avec ses propres particularités.

Cependant, aucune des implémentations Java Core Map existantes n'autorise une Map à gérer plusieurs valeurs pour une seule clé .

Comme nous pouvons le voir, si nous essayons d'insérer deux valeurs pour la même clé, la deuxième valeur sera stockée, tandis que la première sera supprimée.

Il sera également retourné (par chaque implémentation correcte de la méthode put (K key, V value) ):

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

Comment pouvons-nous alors atteindre le comportement souhaité?

3. Collection comme valeur

De toute évidence, utiliser une collection pour chaque valeur de notre carte ferait l'affaire:

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

Cependant, cette solution détaillée présente de multiples inconvénients et est sujette à des erreurs. Cela implique que nous devons instancier une Collection pour chaque valeur, vérifier sa présence avant d'ajouter ou de supprimer une valeur, la supprimer manuellement lorsqu'aucune valeur n'est laissée, etc.

Depuis Java 8, nous pourrions exploiter les méthodes compute () et l'améliorer:

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

Bien que ce soit quelque chose à savoir, nous devons l'éviter à moins d'avoir une très bonne raison de ne pas le faire, comme des politiques d'entreprise restrictives nous empêchant d'utiliser des bibliothèques tierces.

Sinon, avant d'écrire notre propre implémentation Map personnalisée et de réinventer la roue, nous devrions choisir parmi les nombreuses options disponibles prêtes à l'emploi.

4. Collections Apache Commons

Comme d'habitude, Apache a une solution à notre problème.

Commençons par importer la dernière version de Common Collections (CC à partir de maintenant):

 org.apache.commons commons-collections4 4.1 

4.1. MultiMap

Le fichier org.apache.commons.collections4. L' interface MultiMap définit une carte qui contient une collection de valeurs pour chaque clé.

Il est implémenté par org.apache.commons.collections4.map. Classe MultiValueMap , qui gère automatiquement la plupart des passe-partout sous le capot:

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

Bien que cette classe soit disponible depuis CC 3.2, elle n'est pas thread-safe et elle est obsolète dans CC 4.1 . Nous ne devrions l'utiliser que lorsque nous ne pouvons pas passer à la version la plus récente.

4.2. MultiValuedMap

Le successeur de MultiMap est org.apache.commons.collections4. Interface MultiValuedMap . Il a plusieurs implémentations prêtes à être utilisées.

Voyons comment stocker nos multiples valeurs dans une ArrayList , qui conserve les doublons:

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

Alternativement, nous pourrions utiliser un HashSet , qui supprime les doublons:

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

Les deux implémentations ci - dessus ne sont pas thread-safe .

Voyons comment nous pouvons utiliser le décorateur UnmodifiableMultiValuedMap pour les rendre immuables:

@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. Guava Multimap

Guava est la bibliothèque principale de Google pour l'API Java.

Le fichier com.google.common.collect. L' interface Multimap existe depuis la version 2. Au moment de la rédaction de cet article, la dernière version est la 25, mais depuis la version 23, elle a été divisée en différentes branches pour jre et android ( 25.0-jre et 25.0-android ), nous allons toujours utiliser version 23 pour nos exemples.

Commençons par importer Guava sur notre projet:

 com.google.guava guava 23.0 

Guava a suivi le chemin des multiples implémentations depuis le début.

Le plus courant est le com.google.common.collect. ArrayListMultimap , qui utilise un HashMap sauvegardé par un ArrayList pour chaque valeur:

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

Comme toujours, nous devrions préférer les implémentations immuables de l'interface Multimap: com.google.common.collect. ImmutableListMultimap et com.google.common.collect. ImmutableSetMultimap .

5.1. Implémentations de cartes communes

Lorsque nous avons besoin d'une implémentation Map spécifique , la première chose à faire est de vérifier si elle existe, car probablement Guava l'a déjà implémentée.

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.

Nous avons exploré les implémentations les plus populaires d'Apache Commons Collections et Guava, qui devraient être préférées aux solutions personnalisées lorsque cela est possible.

Comme toujours, le code source complet est disponible sur Github.