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:
- Compatibilità con le versioni precedenti. Un numero limitato di classi della libreria standard restituisce
Hashtable—System.getProperties()restituisce un'istanza diProperties, che estendeHashtable<Object, Object>. Alcune vecchie API JNDI (InitialContext(Hashtable)) ne accettano una come argomento. - Codice esistente. Qualsiasi codebase più vecchio di circa il 2005 potrebbe ancora avere
Hashtablein posti dove nessuno ha voluto eseguire la migrazione. - 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:
| Caratteristica | Hashtable | HashMap |
|---|---|---|
| Thread safety | ogni metodo synchronized sull'intera tabella | non thread-safe |
Chiave null | rifiutata (NullPointerException) | una consentita |
Valore null | rifiutato (NullPointerException) | molti consentiti |
| Ordine di iterazione | non specificato | non specificato |
| Capacità predefinita | 11 | 16 |
| Crescita della capacità | 2*old + 1 (dimensioni dispari, modulo più lento) | raddoppia alla potenza di due successiva |
| API pre-collezioni | enumerazioni elements(), keys() | nessuna |
| Treeificazione Java 8 | no | sì — i bucket diventano alberi dopo 8 elementi |
| Iteratori fail-fast | sì, dal retrofit del 1.2 | sì |
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
Map→HashMap. Fine. L'overhead disynchronizeddiHashtableè un costo puro senza benefici. - Codice multi-thread, vuoi una
Map→ConcurrentHashMap. 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 resto →
Collections.synchronizedMap(new HashMap<>()). Stesso comportamento con lock singolo diHashtable, ma si compone con la moderna API delle collezioni. Comunque peggiore diConcurrentHashMapse puoi usarlo. - Stai vedendo un'API che richiede
Hashtable(Properties, JNDI) → usaHashtableperché 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.
Cosa ricavare dall'esecuzione:
- L'API di base è quella di
Map, quindiHashtablesi comporta comeHashMapper un uso semplice. Risultati identici, più lenta. - I null vengono rifiutati su entrambi i lati — è l'unica
Mapnella famiglia JDK che rifiuta i valorinulloltre alle chiavinull. - Il contatore
Hashtableè sbagliato. Ogni metodo è sincronizzato, magetpoiputsono due operazioni atomiche separate, non una sola. I thread si contendono lo spazio nel mezzo e perdono aggiornamenti. - La versione con
ConcurrentHashMapemergeè 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.