W3docs

Java Hashtable

La classe Hashtable sincronizzata in Java, perché è superata da HashMap e ConcurrentHashMap, e quando si incontra.

Hashtable<K, V> è la mappa hash originale di Java, risalente al JDK 1.0 del 1996 — due anni prima che il framework delle collezioni venisse aggiunto. Quando Map, HashMap e il resto arrivarono nel JDK 1.2, Hashtable fu adattata per implementare Map, ma le sue peculiarità rimasero: ogni metodo è synchronized, sia le chiavi che i valori rifiutano null, e l'API pubblica include metodi pre-collezioni (elements(), keys()) che precedono Iterator.

Nel nuovo codice quasi mai la si vorrà usare. Questo capitolo esiste affinché tu riconosca la classe quando la incontri, capisca perché esiste ancora, e sappia cosa usare al suo posto.

Perché esiste ancora

Tre motivi:

  1. Compatibilità con le versioni precedenti. Un numero limitato di classi della libreria standard restituisce HashtableSystem.getProperties() restituisce un'istanza di Properties, che estende Hashtable<Object, Object>. Alcune vecchie API JNDI (InitialContext(Hashtable)) ne accettano una come argomento.
  2. Codice esistente. Qualsiasi codebase più vecchio di circa il 2005 potrebbe ancora avere Hashtable in posti dove nessuno ha voluto eseguire la migrazione.
  3. Familiarità mal riposta. Compare nei colloqui e nei tutorial, e i principianti a volte la scelgono perché "voglio una mappa thread-safe" — senza conoscere ConcurrentHashMap.

Come si differenzia da HashMap

Hashtable e HashMap sono entrambe hash table con chaining, entrambe implementano Map<K, V>, ed entrambe hanno complessità O(1) attesa per get/put/remove. Le differenze:

CaratteristicaHashtableHashMap
Thread safetyogni metodo synchronized sull'intera tabellanon thread-safe
Chiave nullrifiutata (NullPointerException)una consentita
Valore nullrifiutato (NullPointerException)molti consentiti
Ordine di iterazionenon specificatonon specificato
Capacità predefinita1116
Crescita della capacità2*old + 1 (dimensioni dispari, modulo più lento)raddoppia alla potenza di due successiva
API pre-collezionienumerazioni elements(), keys()nessuna
Treeificazione Java 8nosì — i bucket diventano alberi dopo 8 elementi
Iteratori fail-fastsì, dal retrofit del 1.2
clone()sì (superficiale)sì (superficiale)

La sincronizzazione è la differenza principale e il motivo più importante per cui Hashtable è lenta: ogni lettura e ogni scrittura acquisisce lo stesso lock sull'intera tabella. Un programma multi-thread con due thread che fanno soltanto get su una Hashtable viene serializzato — si alternano nel lock.

Perché synchronized su ogni metodo non è vera thread safety

Un bug sorprendentemente comune: gli sviluppatori vedono "ogni metodo è synchronized" e pensano che Hashtable renda corretto il loro codice multi-thread. Non è così. Le operazioni composte sono comunque soggette a race condition:

if (!table.containsKey(key)) {       // synchronized
  table.put(key, computeValue());    // synchronized — but separate lock acquisition
}

Tra le due chiamate, un altro thread può eseguire put sulla stessa chiave. Entrambi i thread vedono containsKey restituire false, entrambi calcolano, entrambi eseguono put. Si ottengono due valutazioni e il valore sbagliato prevale.

La soluzione oggi non è correggere Hashtable, ma usare ConcurrentHashMap, che ha operazioni composte atomiche integrate: putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Acquisiscono i lock giusti internamente ed eseguono il test-and-set come un'unica operazione.

Cosa usare al suo posto

Il flusso decisionale quando sei tentato di scrivere new Hashtable<>():

  • Codice single-thread, vuoi una MapHashMap. Fine. L'overhead di synchronized di Hashtable è un costo puro senza benefici.
  • Codice multi-thread, vuoi una MapConcurrentHashMap. Lock a strisce (lock-free per le letture nei JDK moderni), nessun lock globale, operazioni composte atomiche, scalabilità notevolmente migliore.
  • Codice multi-thread, hai genuinamente bisogno che ogni operazione sia atomica rispetto a tutto il restoCollections.synchronizedMap(new HashMap<>()). Stesso comportamento con lock singolo di Hashtable, ma si compone con la moderna API delle collezioni. Comunque peggiore di ConcurrentHashMap se puoi usarlo.
  • Stai vedendo un'API che richiede Hashtable (Properties, JNDI) → usa Hashtable perché l'API lo richiede; non introdurre una parallela.

Le peculiarità pre-collezioni

Hashtable precede Iterator ed espone Enumeration<K> al suo posto:

Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
  System.out.println(keys.nextElement());
}

Enumeration ha solo hasMoreElements() e nextElement() — nessun remove(). Il retrofit del 1.2 ha aggiunto keySet(), entrySet() e values() da Map, e puoi iterarli con un normale Iterator. Ma poiché entrambe le API esistono sullo stesso oggetto, vedrai entrambi gli stili in natura. Preferisci la vista Map; è il linguaggio che già conosci.

Esempio pratico: Hashtable, perché rifiuta i null e la race condition che non protegge

Il programma seguente illustra le differenze visibili da HashMap — i metodi sincronizzati, i null rifiutati, l'enumerazione legacy — e mostra la race condition check-then-act che la sincronizzazione di Hashtable non risolve.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • L'API di base è quella di Map, quindi Hashtable si comporta come HashMap per un uso semplice. Risultati identici, più lenta.
  • I null vengono rifiutati su entrambi i lati — è l'unica Map nella famiglia JDK che rifiuta i valori null oltre alle chiavi null.
  • Il contatore Hashtable è sbagliato. Ogni metodo è sincronizzato, ma get poi put sono due operazioni atomiche separate, non una sola. I thread si contendono lo spazio nel mezzo e perdono aggiornamenti.
  • La versione con ConcurrentHashMap e merge è corretta e veloce. Questo è lo strumento giusto per "mappa thread-safe" nel 2026.

Cosa viene dopo

Hashtable ha un discendente che utilizzerai effettivamente: Properties, il contenitore di configurazione alla base di System.getProperties() e del formato file .properties. Ha uno scopo limitato e piacevole da usare; è il prossimo capitolo, e l'ultimo capitolo sulle "strutture dati" in questa parte del libro prima di passare all'iterazione, all'ordinamento e ai metodi di utilità statici.

Pratica

Pratica
Il tuo ingegnere senior ti chiede di rimuovere un `Hashtable<String, Integer>` da un contatore multi-thread che esegue `int n = t.get(k); t.put(k, n + 1);`. Qual è la sostituzione corretta?
Il tuo ingegnere senior ti chiede di rimuovere un `Hashtable<String, Integer>` da un contatore multi-thread che esegue `int n = t.get(k); t.put(k, n + 1);`. Qual è la sostituzione corretta?
Was this page helpful?