Java Hashtable
La Hashtable synchronisée héritée en Java, pourquoi elle est remplacée par HashMap et ConcurrentHashMap, et quand elle apparaît.
Hashtable<K, V> est la table de hachage d'origine en Java, remontant au JDK 1.0 en 1996 — deux ans avant l'ajout du framework de collections. Quand Map, HashMap et le reste ont été intégrés dans le JDK 1.2, Hashtable a été rétroactivement adaptée pour implémenter Map, mais ses particularités sont restées : chaque méthode est synchronized, les clés et les valeurs rejettent toutes deux null, et l'API publique comprend des méthodes pré-collections (elements(), keys()) qui précèdent Iterator.
Dans le nouveau code, vous ne voudrez presque jamais l'utiliser. Ce chapitre est là pour que vous reconnaissiez la classe quand vous la rencontrez, que vous compreniez pourquoi elle existe encore, et que vous sachiez quoi utiliser à la place.
Pourquoi elle existe encore
Trois raisons :
- Compatibilité ascendante. Quelques classes de la bibliothèque standard renvoient une
Hashtable—System.getProperties()renvoie une instance deProperties, qui étendHashtable<Object, Object>. Certaines anciennes API JNDI (InitialContext(Hashtable)) en prennent une comme argument. - Le code existant. Toute base de code antérieure à environ 2005 peut encore contenir des
Hashtabledans des endroits où personne n'a eu envie de migrer. - Une familiarité mal placée. Elle apparaît dans les entretiens et les tutoriels, et les débutants y ont parfois recours parce que « je veux une map thread-safe » — sans connaître
ConcurrentHashMap.
En quoi elle diffère de HashMap
Hashtable et HashMap sont toutes deux des tables de hachage avec chaînage, implémentent toutes deux Map<K, V>, et offrent toutes deux une complexité O(1) attendue pour get/put/remove. Les différences :
| Fonctionnalité | Hashtable | HashMap |
|---|---|---|
| Sécurité des threads | chaque méthode synchronized sur l'ensemble de la table | non thread-safe |
Clé null | rejetée (NullPointerException) | une seule autorisée |
Valeur null | rejetée (NullPointerException) | plusieurs autorisées |
| Ordre d'itération | non spécifié | non spécifié |
| Capacité par défaut | 11 | 16 |
| Croissance de la capacité | 2*old + 1 (tailles impaires, modulo plus lent) | double jusqu'à la prochaine puissance de deux |
| API pré-collections | énumérations elements(), keys() | aucune |
| Arbification Java 8 | non | oui — les buckets deviennent des arbres au-delà de 8 entrées |
| Itérateurs fail-fast | oui, depuis la rétrocompatibilité 1.2 | oui |
clone() | oui (superficiel) | oui (superficiel) |
La synchronisation est la différence principale et la principale raison pour laquelle Hashtable est lente : chaque lecture et chaque écriture acquiert le même verrou sur l'ensemble de la table. Un programme multi-thread avec deux threads qui ne font que des get contre une Hashtable est sérialisé — ils se relaient sur le verrou.
Pourquoi synchronized sur chaque méthode n'est pas une vraie sécurité des threads
Un bug étonnamment courant : les développeurs voient « chaque méthode est synchronisée » et pensent que Hashtable rend leur code multi-thread correct. Ce n'est pas le cas. Les opérations composées restent sujettes aux conditions de course :
if (!table.containsKey(key)) { // synchronized
table.put(key, computeValue()); // synchronized — but separate lock acquisition
}Entre les deux appels, un autre thread peut faire un put de la même clé. Les deux threads voient containsKey renvoyer false, calculent tous les deux, et font tous les deux un put. Vous obtenez deux évaluations et la mauvaise valeur l'emporte.
La solution aujourd'hui n'est pas de corriger Hashtable mais d'utiliser ConcurrentHashMap, qui dispose d'opérations composées atomiques intégrées : putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Elles acquièrent les bons verrous en interne et effectuent le test-and-set en une seule opération.
Quoi utiliser à la place
L'arbre de décision quand vous êtes tenté d'écrire new Hashtable<>() :
- Code mono-thread, vous voulez une
Map→HashMap. Arrêtez-vous là. Le surcoût desynchronizeddeHashtableest un coût pur sans aucun bénéfice. - Code multi-thread, vous voulez une
Map→ConcurrentHashMap. Verrous par bandes (sans verrou pour les lectures dans les JDK modernes), pas de verrou global, opérations composées atomiques, bien meilleure scalabilité. - Code multi-thread, vous avez vraiment besoin que chaque opération soit atomique avec tout le reste →
Collections.synchronizedMap(new HashMap<>()). Même comportement à verrou unique queHashtablemais compatible avec l'API collections moderne. Reste moins performant queConcurrentHashMapsi vous pouvez l'utiliser. - Vous voyez une API qui nécessite
Hashtable(Properties, JNDI) → utilisez laHashtableparce que l'API l'exige ; n'en introduisez pas une en parallèle.
Les particularités pré-collections
Hashtable est antérieure à Iterator et expose Enumeration<K> à la place :
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
}Enumeration n'a que hasMoreElements() et nextElement() — pas de remove(). La rétrocompatibilité en 1.2 a ajouté keySet(), entrySet() et values() depuis Map, et vous pouvez les parcourir avec un Iterator normal. Mais comme les deux API existent sur le même objet, vous verrez les deux styles dans la pratique. Préférez la vue Map ; c'est le langage que vous connaissez déjà.
Exemple complet : Hashtable, pourquoi elle rejette les nulls, et la condition de course qu'elle ne protège pas
Le programme ci-dessous illustre les différences visibles par rapport à HashMap — les méthodes synchronisées, les nulls rejetés, l'énumération héritée — et montre la condition de course de type vérifier-puis-agir que la synchronisation de Hashtable ne résout pas.
Ce qu'il faut retenir de l'exécution :
- L'API de base est celle de
Map, doncHashtableressemble àHashMappour un usage simple. Résultats identiques, mais plus lent. - Les nulls sont rejetés des deux côtés — c'est la seule
Mapde la famille JDK qui rejette les valeurs null en plus des clés null. - Le compteur
Hashtableest faux. Chaque méthode est synchronisée, maisgetpuisputsont deux opérations atomiques séparées, pas une seule. Les threads font la course entre les deux et perdent des mises à jour. - La version
ConcurrentHashMapavecmergeest correcte et rapide. C'est le bon outil pour « une map thread-safe » en 2026.
Prochaine étape
Hashtable a un descendant que vous utiliserez vraiment : Properties, le conteneur de configuration derrière System.getProperties() et le format de fichier .properties. Sa portée est limitée et agréable à utiliser ; c'est le prochain chapitre, et le dernier chapitre « structure de données » dans cette partie du livre avant de passer à l'itération, au tri et aux méthodes utilitaires statiques.