W3docs

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:

  • put restituisce il valore precedente per quella chiave, o null se non era presente alcuna mappatura. È così che si implementano gli idiomi "inserisci se assente" — tranne che non è necessario farlo, perché putIfAbsent fa esattamente questo ed è più chiaro.

  • get che restituisce null significa o "la chiave non è presente" oppure "la chiave è presente ma il suo valore è null." È un'ambiguità se la tua mappa consente valori null; usa containsKey per disambiguare, oppure — meglio — usa getOrDefault per 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 con def invece di null.
  • putIfAbsent(k, v)put solo 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:

Classechiave nullvalore null
HashMapuna consentitamolti consentiti
LinkedHashMapuna consentitamolti consentiti
TreeMapnomolti consentiti
Hashtablenono
ConcurrentHashMapnono
Map.of(...) (immutabile)nono

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

ClasseStruttura sottostanteOrdine di iterazioneUtilizzo
HashMaphash tablenon specificatol'impostazione predefinita
LinkedHashMaphash table + linked listordine di inserimento o accessocache LRU, iterazione prevedibile
TreeMapred-black treeordinato per chiaverange query sulle chiavi, output ordinato
Hashtablehash table, sincronizzatanon specificatolegacy; raramente la scelta giusta
ConcurrentHashMapstriped hash tablenon specificatocodice multi-thread
EnumMapbit-array-indexedordine enumMap<MyEnum, V>
Map.of(...)immutabilenon specificatomappe 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.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • merge(word, 1, Integer::sum) è il modo moderno e idiomatico per contare le parole. Nessun get/put/controllo-null da nessuna parte.
  • computeIfAbsent crea la lista vuota esattamente una volta per chiave — un modo pulito per costruire una Map<K, List<V>> senza disseminare if (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.
  • getOrDefault elimina il motivo più comune per controllare il null. Usalo ogni volta che esiste un valore predefinito sensato.
  • Una HashMap e una TreeMap con le stesse entry sono equals tra 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.

Esercitazione

Pratica
`counts` è una `HashMap<String, Integer>`. Quale riga è il modo idiomatico per incrementare il conteggio di `'java'`, trattando una chiave assente come se partisse da zero?
`counts` è una `HashMap<String, Integer>`. Quale riga è il modo idiomatico per incrementare il conteggio di `'java'`, trattando una chiave assente come se partisse da zero?
Was this page helpful?