W3docs

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 :

  1. Compatibilité ascendante. Quelques classes de la bibliothèque standard renvoient une HashtableSystem.getProperties() renvoie une instance de Properties, qui étend Hashtable<Object, Object>. Certaines anciennes API JNDI (InitialContext(Hashtable)) en prennent une comme argument.
  2. Le code existant. Toute base de code antérieure à environ 2005 peut encore contenir des Hashtable dans des endroits où personne n'a eu envie de migrer.
  3. 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éHashtableHashMap
Sécurité des threadschaque méthode synchronized sur l'ensemble de la tablenon thread-safe
Clé nullrejetée (NullPointerException)une seule autorisée
Valeur nullrejetée (NullPointerException)plusieurs autorisées
Ordre d'itérationnon spécifiénon spécifié
Capacité par défaut1116
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 8nonoui — les buckets deviennent des arbres au-delà de 8 entrées
Itérateurs fail-fastoui, depuis la rétrocompatibilité 1.2oui
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 MapHashMap. Arrêtez-vous là. Le surcoût de synchronized de Hashtable est un coût pur sans aucun bénéfice.
  • Code multi-thread, vous voulez une MapConcurrentHashMap. 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 resteCollections.synchronizedMap(new HashMap<>()). Même comportement à verrou unique que Hashtable mais compatible avec l'API collections moderne. Reste moins performant que ConcurrentHashMap si vous pouvez l'utiliser.
  • Vous voyez une API qui nécessite Hashtable (Properties, JNDI) → utilisez la Hashtable parce 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • L'API de base est celle de Map, donc Hashtable ressemble à HashMap pour un usage simple. Résultats identiques, mais plus lent.
  • Les nulls sont rejetés des deux côtés — c'est la seule Map de la famille JDK qui rejette les valeurs null en plus des clés null.
  • Le compteur Hashtable est faux. Chaque méthode est synchronisée, mais get puis put sont 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 ConcurrentHashMap avec merge est 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.

Pratique

Pratique
Votre ingénieur senior vous demande de supprimer un `Hashtable<String, Integer>` d'un compteur multi-thread qui fait `int n = t.get(k); t.put(k, n + 1);`. Quel est le bon remplacement ?
Votre ingénieur senior vous demande de supprimer un `Hashtable<String, Integer>` d'un compteur multi-thread qui fait `int n = t.get(k); t.put(k, n + 1);`. Quel est le bon remplacement ?
Was this page helpful?