Java Hashtable
Die legacy-synchronisierte Hashtable in Java, warum sie durch HashMap und ConcurrentHashMap abgelöst wurde und wann sie noch auftaucht.
Hashtable<K, V> ist die ursprüngliche hash-basierte Map in Java, die bis auf JDK 1.0 aus dem Jahr 1996 zurückgeht — zwei Jahre vor der Einführung des Collections Frameworks. Als Map, HashMap und die übrigen Klassen in JDK 1.2 hinzukamen, wurde Hashtable nachträglich so angepasst, dass es Map implementiert, aber seine Eigenheiten blieben erhalten: jede Methode ist synchronized, sowohl Schlüssel als auch Werte lehnen null ab, und die öffentliche API enthält Pre-Collections-Methoden (elements(), keys()), die vor Iterator existierten.
In neuem Code möchte man sie fast nie verwenden. Dieses Kapitel ist dazu da, damit Sie die Klasse erkennen, wenn Sie ihr begegnen, verstehen, warum sie noch existiert, und wissen, was Sie stattdessen verwenden sollten.
Warum sie noch existiert
Drei Gründe:
- Abwärtskompatibilität. Eine Handvoll Klassen der Standardbibliothek gibt
Hashtablezurück —System.getProperties()gibt eineProperties-Instanz zurück, dieHashtable<Object, Object>erweitert. Einige alte JNDI-APIs (InitialContext(Hashtable)) nehmen sie als Argument. - Vorhandener Code. Jede Codebasis, die älter als etwa 2005 ist, kann noch
Hashtablean Stellen enthalten, an denen niemand eine Migration vornehmen wollte. - Fehlgeleitete Vertrautheit. Sie taucht in Interviews und Tutorials auf, und Anfänger greifen manchmal danach, weil sie "eine thread-sichere Map möchten" — ohne von
ConcurrentHashMapzu wissen.
Unterschiede zu HashMap
Hashtable und HashMap sind beide Hash-Tabellen mit Chaining, implementieren beide Map<K, V> und haben beide O(1) erwartet für get/put/remove. Die Unterschiede:
| Merkmal | Hashtable | HashMap |
|---|---|---|
| Thread-Sicherheit | jede Methode synchronized auf die gesamte Tabelle | nicht thread-sicher |
null-Schlüssel | abgelehnt (NullPointerException) | einer erlaubt |
null-Wert | abgelehnt (NullPointerException) | viele erlaubt |
| Iterationsreihenfolge | nicht festgelegt | nicht festgelegt |
| Standardkapazität | 11 | 16 |
| Kapazitätswachstum | 2*old + 1 (ungerade Größen, langsameres Modulo) | verdoppelt auf die nächste Zweierpotenz |
| Pre-Collections-API | elements(), keys() Enumerations | keine |
| Java 8 Baumifizierung | nein | ja — Buckets werden bei über 8 Einträgen zu Bäumen |
| Fail-Fast-Iteratoren | ja, seit Retrofit 1.2 | ja |
clone() | ja (flach) | ja (flach) |
Die Synchronisierung ist der entscheidende Unterschied und der Hauptgrund, warum Hashtable langsam ist: jeder Lese- und jeder Schreibzugriff erwirbt die gleiche Sperre auf die gesamte Tabelle. Ein Multithreading-Programm mit zwei Threads, die nichts anderes tun als get auf einer Hashtable, wird serialisiert — sie wechseln sich mit der Sperre ab.
Warum synchronized auf jeder Methode keine echte Thread-Sicherheit ist
Ein überraschend häufiger Fehler: Entwickler sehen "jede Methode ist synchronized" und denken, Hashtable macht ihren Multithreading-Code korrekt. Das tut es nicht. Zusammengesetzte Operationen sind immer noch fehlerhaft:
if (!table.containsKey(key)) { // synchronized
table.put(key, computeValue()); // synchronized — but separate lock acquisition
}Zwischen den beiden Aufrufen kann ein anderer Thread den gleichen Schlüssel mit put einfügen. Beide Threads sehen, dass containsKey false zurückgibt, beide berechnen, beide führen put aus. Sie erhalten zwei Auswertungen und der falsche Wert gewinnt.
Die heutige Lösung besteht nicht darin, Hashtable zu reparieren, sondern ConcurrentHashMap zu verwenden, das atomare zusammengesetzte Operationen eingebaut hat: putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Sie erwerben intern die richtigen Sperren und führen das Test-und-Setzen als eine einzige Operation durch.
Was man stattdessen verwenden sollte
Der Entscheidungsablauf, wenn Sie versucht sind, new Hashtable<>() zu schreiben:
- Single-Thread-Code, Sie möchten eine
Map→HashMap. Fertig. Dersynchronized-Overhead vonHashtableist reiner Kostenpunkt ohne Nutzen. - Multithreading-Code, Sie möchten eine
Map→ConcurrentHashMap. Lock-striped (lock-frei für Lesezugriffe in modernen JDKs), keine globale Sperre, atomare zusammengesetzte Operationen, weit bessere Skalierbarkeit. - Multithreading-Code, Sie benötigen wirklich jede Operation atomar mit allem anderen →
Collections.synchronizedMap(new HashMap<>()). Gleiche Einzelsperren-Verhalten wieHashtable, verbindet sich aber mit der modernen Collections-API. Immer noch schlechter alsConcurrentHashMap, wenn Sie das verwenden können. - Sie sehen eine API, die
Hashtableerfordert (Properties, JNDI) → verwenden Sie dieHashtable, weil die API es erfordert; führen Sie keine parallele ein.
Die Pre-Collections-Eigenheiten
Hashtable existierte vor Iterator und stellt stattdessen Enumeration<K> bereit:
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
}Enumeration hat nur hasMoreElements() und nextElement() — kein remove(). Der Retrofit in 1.2 fügte keySet(), entrySet() und values() aus Map hinzu, und Sie können diese mit einem normalen Iterator durchlaufen. Da aber beide APIs auf demselben Objekt vorhanden sind, sehen Sie beide Stile in der Praxis. Bevorzugen Sie die Map-Ansicht; das ist die Sprache, die Sie bereits kennen.
Ein ausgearbeitetes Beispiel: Hashtable, warum sie Nulls ablehnt und die Race Condition, gegen die sie nicht schützt
Das folgende Programm demonstriert die sichtbaren Unterschiede zu HashMap — die synchronisierten Methoden, die abgelehnten Nulls, die Legacy-Enumeration — und zeigt die Check-then-Act-Race Condition, die die Synchronisierung von Hashtable nicht löst.
Was man aus der Ausführung mitnehmen kann:
- Die grundlegende API ist die von
Map, daher siehtHashtablebei einfacher Verwendung wieHashMapaus. Identische Ergebnisse, aber langsamer. - Nulls werden auf beiden Seiten abgelehnt — das ist die einzige
Mapin der JDK-Familie, die null-Werte sowie null-Schlüssel ablehnt. - Der
Hashtable-Zähler ist falsch. Jede Methode ist synchronized, abergetdannputsind zwei separate atomare Operationen, nicht eine. Threads konkurrieren dazwischen und verlieren Aktualisierungen. - Die
ConcurrentHashMap-Version mitmergeist korrekt und schnell. Das ist das richtige Werkzeug für "thread-sichere Map" im Jahr 2026.
Was kommt als nächstes
Hashtable hat einen Nachkommen, den Sie tatsächlich verwenden werden: Properties, den Konfigurationscontainer hinter System.getProperties() und dem .properties-Dateiformat. Es ist in seinem Anwendungsbereich eng und angenehm zu verwenden; es ist das nächste Kapitel und das letzte "Datenstruktur"-Kapitel in diesem Teil des Buches, bevor wir zu Iteration, Sortierung und den statischen Hilfsmethoden übergehen.