Come memorizzare chiavi duplicate in una mappa in Java?

1. Panoramica

In questo tutorial, esploreremo le opzioni disponibili per gestire una mappa con chiavi duplicate o, in altre parole, una mappa che consente di memorizzare più valori per una singola chiave.

2. Mappe standard

Java ha diverse implementazioni dell'interfaccia Map , ognuna con le proprie particolarità.

Tuttavia, nessuna delle implementazioni di Java core Map esistenti consente a una mappa di gestire più valori per una singola chiave .

Come possiamo vedere, se proviamo a inserire due valori per la stessa chiave, il secondo valore verrà memorizzato, mentre il primo verrà eliminato.

Verrà anche restituito (da ogni corretta implementazione del metodo put (tasto K, valore 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"); 

Come possiamo quindi ottenere il comportamento desiderato?

3. Raccolta come valore

Ovviamente, utilizzare una raccolta per ogni valore della nostra mappa farebbe il lavoro:

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

Tuttavia, questa soluzione dettagliata presenta diversi inconvenienti ed è soggetta a errori. Ciò implica che dobbiamo creare un'istanza di una raccolta per ogni valore, verificarne la presenza prima di aggiungere o rimuovere un valore, eliminarlo manualmente quando non sono rimasti valori, eccetera.

Da Java 8 potremmo sfruttare i metodi compute () e migliorarlo:

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

Sebbene questo sia qualcosa che vale la pena sapere, dovremmo evitarlo a meno che non abbia un ottimo motivo per non farlo, come le politiche aziendali restrittive che ci impediscono di utilizzare le librerie di terze parti.

Altrimenti, prima di scrivere la nostra implementazione della mappa personalizzata e reinventare la ruota, dovremmo scegliere tra le diverse opzioni disponibili immediatamente.

4. Collezioni Apache Commons

Come al solito, Apache ha una soluzione per il nostro problema.

Cominciamo importando l'ultima versione di Common Collections (CC d'ora in poi):

 org.apache.commons commons-collections4 4.1 

4.1. MultiMap

L'org.apache.commons.collections4. L' interfaccia MultiMap definisce una mappa che contiene una raccolta di valori per ciascuna chiave.

È implementato da org.apache.commons.collections4.map. Classe MultiValueMap , che gestisce automaticamente la maggior parte del boilerplate sotto il cofano:

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

Sebbene questa classe sia disponibile da CC 3.2, non è thread-safe ed è stata deprecata in CC 4.1 . Dovremmo usarlo solo quando non possiamo eseguire l'aggiornamento alla versione più recente.

4.2. MultiValuedMap

Il successore di MultiMap è org.apache.commons.collections4. Interfaccia MultiValuedMap . Ha più implementazioni pronte per essere utilizzate.

Vediamo come memorizzare i nostri più valori in un ArrayList , che conserva i duplicati:

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

In alternativa, potremmo usare un HashSet , che rilascia i duplicati:

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

Entrambe le implementazioni precedenti non sono thread-safe .

Vediamo come possiamo utilizzare il decoratore UnmodifiableMultiValuedMap per renderli immutabili:

@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 è l'API Google Core Libraries for Java.

Il com.google.common.collect. L' interfaccia Multimap è disponibile dalla versione 2. Al momento della scrittura l'ultima versione è la 25, ma poiché dopo la versione 23 è stata suddivisa in diversi rami per jre e android ( 25.0-jre e 25.0-android ), useremo ancora versione 23 per i nostri esempi.

Cominciamo importando Guava nel nostro progetto:

 com.google.guava guava 23.0 

Guava ha seguito il percorso delle molteplici implementazioni sin dall'inizio.

Il più comune è com.google.common.collect. ArrayListMultimap , che utilizza una HashMap supportata da un ArrayList per ogni valore:

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

Come sempre, dovremmo preferire le implementazioni immutabili dell'interfaccia Multimap: com.google.common.collect. ImmutableListMultimap e com.google.common.collect. ImmutableSetMultimap .

5.1. Implementazioni di mappe comuni

Quando abbiamo bisogno di una specifica implementazione di Map , la prima cosa da fare è controllare se esiste, perché probabilmente Guava l'ha già implementata.

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.

Abbiamo esplorato le implementazioni più popolari di Apache Commons Collections e Guava, che dovrebbero essere preferite alle soluzioni personalizzate quando possibile.

Come sempre, il codice sorgente completo è disponibile su Github.