Interfaccia Map in Java
Mappature chiave-valore in Java con l'interfaccia Map — put, get, remove, keySet, values, entrySet.
Questo capitolo tratta il contratto Map: i suoi sette metodi principali, le tre viste che espone per l'iterazione, i metodi predefiniti di Java 8 che rendono il codice moderno per le mappe conciso, le regole di gestione dei null per implementazione e come le mappe si confrontano per uguaglianza. Al termine saprai quali idiomi usare e quale implementazione standard si adatta a un determinato contesto.
Map<K, V> è l'altra metà del collections framework. A differenza dell'interfaccia Collection, non estende Collection — è una gerarchia separata, perché memorizzare chiavi associate a valori è un'astrazione diversa rispetto al memorizzare un insieme di elementi. Internamente la maggior parte delle implementazioni Set sono semplicemente Map in cui si ignora il valore, quindi Map è in un certo senso la struttura primaria e Set è il fratello più semplice.
Il contratto è breve: ogni chiave mappa ad al più un valore, le chiavi formano un insieme (nessuna chiave duplicata) e i valori sono una collezione arbitraria (i valori duplicati sono ammessi). Ciò che cambia tra le implementazioni è l'ordine di iterazione, la gestione dei null, le invarianti di ordinamento e la thread safety — ma i sette metodi principali qui sotto si comportano allo stesso modo su tutte.
I sette metodi principali
V put(K key, V value); // insert or overwrite; returns previous value or null
V get(Object key); // lookup; returns null if missing
V remove(Object key); // delete; returns previous value or null
boolean containsKey(Object k); // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();Alcune sottigliezze da interiorizzare:
-
putrestituisce il valore precedente per quella chiave, onullse non era presente alcuna mappatura. È così che si implementano gli idiomi "inserisci se assente" — tranne che non è necessario farlo, perchéputIfAbsentfa esattamente questo ed è più chiaro. -
getche restituiscenullsignifica o "la chiave non è presente" oppure "la chiave è presente ma il suo valore ènull." È un'ambiguità se la tua mappa consente valori null; usacontainsKeyper disambiguare, oppure — meglio — usagetOrDefaultper fornire un valore sentinella:int count = counts.getOrDefault("java", 0); // 0 if absent
Le tre viste
Una Map non è direttamente iterabile. Per iterare, si richiede una delle tre viste del suo contenuto:
Set<K> keys = map.keySet();
Collection<V> values = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();Queste viste sono live — riflettono le modifiche alla mappa sottostante, e le modifiche apportate tramite la vista si propagano indietro. Rimuovere un'entry tramite entrySet() la rimuove dalla mappa; iterare keySet() e chiamare iterator.remove() rimuove l'entry. Non si può fare add a keySet o values (non c'è valore o chiave con cui accoppiare), ma si può fare clear o remove.
L'iterazione usa quasi sempre entrySet() — ottenere entrambe le parti di ogni coppia in una sola volta è più economico che chiamare get(k) per ogni chiave:
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}Oppure, la forma lambda aggiunta in Java 8:
counts.forEach((k, v) -> System.out.println(k + " -> " + v));I metodi predefiniti di Java 8 che contano davvero
Java 8 ha aggiunto diversi metodi Map che accettano una funzione e si comportano atomicamente. Trasformano molti pattern a tre righe in one-liner:
getOrDefault(k, def)—get(k)ma condefinvece dinull.putIfAbsent(k, v)—putsolo se la chiave è assente.computeIfAbsent(k, fn)— calcola atomicamente il valore se assente, lo memorizza, lo restituisce. Il pilastro del pattern "memoize questa chiamata costosa":Map<String, List<Order>> byUser = new HashMap<>(); byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);computeIfPresent(k, biFn)— ricalcola solo se la chiave esiste già. Utile per contatori che devono ignorare le chiavi non ancora viste.compute(k, biFn)— universale: passa il valore corrente (o null), ottieni quello nuovo. Rimuove l'entry se la funzione restituisce null.merge(k, v, biFn)— combina un nuovo valore con quello esistente, se presente. Il classico contatore:for (String w : words) { counts.merge(w, 1, Integer::sum); // first time: stores 1; subsequent: adds }
Queste sono le operazioni che rendono la gestione moderna delle mappe Java concisa. Usale al posto di coppie get/put.
Chiavi null e valori null
Le regole dipendono dall'implementazione:
| Classe | chiave null | valore null |
|---|---|---|
HashMap | una consentita | molti consentiti |
LinkedHashMap | una consentita | molti consentiti |
TreeMap | no | molti consentiti |
Hashtable | no | no |
ConcurrentHashMap | no | no |
Map.of(...) (immutabile) | no | no |
La regola generale per il nuovo codice: non memorizzare null in una mappa. Usa Optional, un valore sentinella, o semplicemente non inserire l'entry. La factory Map.of lo impone per te.
Uguaglianza tra implementazioni
Due mappe sono equals se i loro entrySet() sono uguali — stesse chiavi, stessi valori, indipendentemente dall'ordine di iterazione o dall'implementazione. Una HashMap e una TreeMap con le stesse coppie chiave-valore risultano uguali. È la stessa regola di "uguaglianza strutturale" che segue Set.
Le implementazioni standard, in sintesi
| Classe | Struttura sottostante | Ordine di iterazione | Utilizzo |
|---|---|---|---|
HashMap | hash table | non specificato | l'impostazione predefinita |
LinkedHashMap | hash table + linked list | ordine di inserimento o accesso | cache LRU, iterazione prevedibile |
TreeMap | red-black tree | ordinato per chiave | range query sulle chiavi, output ordinato |
Hashtable | hash table, sincronizzata | non specificato | legacy; raramente la scelta giusta |
ConcurrentHashMap | striped hash table | non specificato | codice multi-thread |
EnumMap | bit-array-indexed | ordine enum | Map<MyEnum, V> |
Map.of(...) | immutabile | non specificato | mappe fisse di piccole dimensioni |
I prossimi capitoli trattano in profondità le scelte quotidiane: HashMap, LinkedHashMap e TreeMap. ConcurrentHashMap ed EnumMap vengono trattati in parti successive.
Un esempio pratico: contatori, raggruppamento e le tre viste
Il programma seguente mostra gli idiomi moderni per le mappe — merge per il conteggio, computeIfAbsent per il raggruppamento, tutte e tre le viste e la distinzione tra getOrDefault e get.
Cosa ricavare dall'esecuzione:
merge(word, 1, Integer::sum)è il modo moderno e idiomatico per contare le parole. Nessunget/put/controllo-null da nessuna parte.computeIfAbsentcrea la lista vuota esattamente una volta per chiave — un modo pulito per costruire unaMap<K, List<V>>senza disseminareif (m.get(k) == null) m.put(k, new ArrayList<>())ovunque.- Le tre viste sono finestre live sulla stessa mappa;
entrySet()è il modo più economico per iterare quando si hanno bisogno di entrambe le metà di ogni coppia. getOrDefaultelimina il motivo più comune per controllare il null. Usalo ogni volta che esiste un valore predefinito sensato.- Una
HashMape unaTreeMapcon le stesse entry sonoequalstra loro; l'unica cosa che cambia è l'ordine di iterazione.
Cosa c'è dopo
L'implementazione predefinita — e quella che vedrai nel 90% del codice Java — è basata su hash table. HashMap è il prossimo capitolo; tratteremo l'array di bucket, l'ottimizzazione di treeification di Java 8 e cosa fare quando le chiavi sono classi personalizzate.