Java Tutorial
🔍

Java Hashtable

Java Hashtable

java.util.Hashtable<K, V> is the thread-safe key-value store that shipped with Java 1.0 — over a decade before ConcurrentHashMap existed. Every method acquires the intrinsic lock on the Hashtable instance before executing, null keys and null values are rejected, and the iteration API uses Enumeration rather than Iterator. It solved the right problem with the tools available in 1996. For new code written today, it is outclassed by every alternative.

Understanding Hashtable matters because it is one of the most common interview comparison topics, and it appears throughout legacy Java EE codebases.

What Is Java Hashtable?

Hashtable<K, V> is a concrete class in java.util that implements Map<K, V>. It stores key-value entries in a hash table — identical in structure to HashMap — but with every public method declared synchronized. It extends Dictionary<K, V>, an abstract class that predates the Collections Framework and is now considered obsolete.

java.util.Dictionary<K, V>             ← abstract class (Java 1.0, obsolete)
    └── java.util.Hashtable<K, V>      ← THIS CLASS
            └── java.util.Properties   ← subclass for .properties file config

java.util.Map<K, V>
    ├── java.util.HashMap<K, V>        ← NOT synchronised (modern, preferred)
    │       └── LinkedHashMap<K, V>
    └── java.util.Hashtable<K, V>      ← synchronised (legacy)

KEY FACTS:
  Package          : java.util
  Since            : Java 1.0  (predates Collections Framework — Java 1.2)
  Implements       : Map<K,V>, Cloneable, Serializable
  Extends          : Dictionary<K,V>  (obsolete abstract class)
  Backing structure: Entry[] table (hash table with chaining)
  Synchronisation  : YES — every public method is synchronized
  Null keys        : NOT allowed — NullPointerException on put(null, value)
  Null values      : NOT allowed — NullPointerException on put(key, null)
  Ordering         : no guaranteed iteration order
  Legacy API       : keys() / elements() return Enumeration
  Default capacity : 11 (HashMap uses 16 — always a power of 2)
  Load factor      : 0.75 (same as HashMap)
  Growth strategy  : newCapacity = oldCapacity × 2 + 1 (always odd)
  Status           : LEGACY — avoid in new code

Basic Overview — Full Hashtable API

HASHTABLE METHOD REFERENCE:

  MAP INTERFACE METHODS (all synchronised in Hashtable):
    put(key, value)          ← insert or update; returns old value
    get(key)                 ← lookup; null = absent (never = null value)
    remove(key)              ← delete entry by key
    containsKey(key)         ← key existence check
    containsValue(value)     ← value existence check (O(n) scan)
    size()                   ← number of entries
    isEmpty()                ← true if size == 0
    keySet()                 ← Set<K> view of all keys
    values()                 ← Collection<V> view of all values
    entrySet()               ← Set<Map.Entry<K,V>> view
    putAll(map)              ← bulk put
    clear()                  ← remove all entries
    forEach(BiConsumer)      ← Java 8+ iteration
    getOrDefault(key, def)   ← Java 8+ safe get

  HASHTABLE-SPECIFIC LEGACY METHODS (pre-Collections Framework):
    keys()                   ← Enumeration<K> of all keys
    elements()               ← Enumeration<V> of all values
    contains(value)          ← same as containsValue() — tests by VALUE
    rehash()                 ← protected; manually trigger resize

NULL POLICY (strict — both key and value rejected):
  put(null, "value") → NullPointerException
  put("key", null)   → NullPointerException
  HashMap: allows one null key, unlimited null values
  ConcurrentHashMap: allows neither (same as Hashtable)

CAPACITY GROWTH:
  Hashtable: newCapacity = (oldCapacity × 2) + 1  (keeps capacity odd)
  HashMap:   newCapacity = oldCapacity × 2         (always power of 2)
  The odd-capacity strategy was an early optimisation for hash distribution.
  HashMap's power-of-2 strategy allows faster bitwise-AND indexing.

When to Use Hashtable

DO NOT USE Hashtable IN NEW CODE.

The Java documentation states that HashMap should be used in place
of Hashtable for non-synchronised use cases, and ConcurrentHashMap
for synchronised use cases. Here is why:

PROBLEM 1 — Global lock kills concurrency:
  Every method acquires the same object lock.
  100 reader threads calling get() execute one at a time.
  ConcurrentHashMap reads are lock-free — all 100 proceed simultaneously.

PROBLEM 2 — Null rejection breaks standard Map contract:
  The general Map contract allows null keys and values (HashMap).
  Hashtable's null rejection makes it a DROP-IN REPLACEMENT FAILURE:
  Code that stores null values with HashMap will break with Hashtable.

PROBLEM 3 — Per-method sync does not protect compound operations:
  if (!ht.containsKey("session")) { ht.put("session", token); }
  → race condition: two threads both pass the containsKey check
  → both execute put() — one value lost
  ConcurrentHashMap.putIfAbsent() handles this atomically.

PROBLEM 4 — Enumeration iteration is obsolete:
  ht.keys() returns Enumeration — no remove() support, verbose syntax.

WHEN Hashtable IS ENCOUNTERED:
  - Legacy Java EE codebases (pre-Java 5)
  - java.util.Properties (a Hashtable subclass) — still used today
  - JNDI, Servlet API, and some JDBC drivers use Hashtable internally
  - Legacy frameworks built before 2004

MODERN ALTERNATIVES:
  Single-threaded Map          → HashMap (fastest, no lock overhead)
  Insertion-order iteration    → LinkedHashMap
  Sorted keys                  → TreeMap
  Thread-safe Map              → ConcurrentHashMap (recommended)
  Thread-safe, ordered keys    → ConcurrentSkipListMap
  Legacy drop-in               → Collections.synchronizedMap(new HashMap<>())

How Hashtable Works Internally

HASHTABLE INTERNAL STRUCTURE:

  private transient Entry<?,?>[] table;  ← the bucket array
  private transient int count;           ← number of entries
  private int threshold;                 ← resize trigger = capacity * loadFactor
  private float loadFactor;             ← default 0.75
  private final int initialCapacity;    ← default 11

  Each Entry<K,V>:
    final int hash        ← stored hash of the key
    final K   key
    V         value
    Entry<?,?> next       ← chain for collision handling (separate chaining)

BUCKET INDEX CALCULATION:
  Hashtable (Java legacy):
    hash  = key.hashCode()        ← raw hashCode, NO bit-mixing
    index = (hash & 0x7FFFFFFF) % table.length   ← modulo (slower)

  HashMap (modern):
    hash  = key.hashCode() ^ (h >>> 16) ← spreads high bits into low bits
    index = hash & (capacity - 1)        ← bitwise AND (faster, requires 2^n)

WHY HASHTABLE KEEPS ODD CAPACITY:
  Modulo with an odd number produces better distribution than modulo with
  an even number for certain hash patterns. This was a pragmatic choice
  for the limited hashCode() implementations of 1996.
  HashMap's approach (power-of-2 + XOR spreading) superseded this.

SYNCHRONISATION:
  public synchronized V put(K key, V value) { ... }
  public synchronized V get(Object key)     { ... }
  public synchronized V remove(Object key)  { ... }
  public synchronized int size()            { ... }
  public synchronized boolean isEmpty()     { ... }
  public synchronized Enumeration<K> keys() { ... }
  // ALL methods — including size() — hold the lock

  Consequence: size() blocks while a put() is running.
  In HashMap: size() reads a plain int field — no lock needed.

Core Operations with Examples

Basic Operations — Map Interface Methods

Java
1// File: HashtableBasicsDemo.java 2 3import java.util.Enumeration; 4import java.util.Hashtable; 5 6public class HashtableBasicsDemo { 7 8 public static void main(String[] args) { 9 10 // Modern Map API — all methods are synchronised 11 Hashtable<String, Integer> productStock = new Hashtable<>(); 12 productStock.put("Laptop", 42); 13 productStock.put("Mouse", 150); 14 productStock.put("Keyboard", 87); 15 productStock.put("Monitor", 25); 16 productStock.put("Webcam", 63); 17 18 System.out.println("=== Map interface methods (all synchronised) ==="); 19 System.out.println("Hashtable : " + productStock); 20 System.out.println("size() : " + productStock.size()); 21 System.out.println("get(Laptop) : " + productStock.get("Laptop")); 22 System.out.println("containsKey(Mouse) : " + productStock.containsKey("Mouse")); 23 System.out.println("get(Tablet) : " + productStock.get("Tablet")); // null = absent 24 25 // put() returns old value (or null if new key) 26 Integer old = productStock.put("Laptop", 50); // update stock 27 System.out.println("Updated Laptop stock, old value was: " + old); 28 System.out.println("New Laptop stock: " + productStock.get("Laptop")); 29 30 System.out.println(); 31 32 // Null key and null value are BOTH rejected 33 System.out.println("=== Null rejection (both key and value) ==="); 34 try { 35 productStock.put(null, 10); 36 } catch (NullPointerException e) { 37 System.out.println("put(null key) throws NullPointerException"); 38 } 39 try { 40 productStock.put("Speaker", null); 41 } catch (NullPointerException e) { 42 System.out.println("put(null value) throws NullPointerException"); 43 } 44 45 System.out.println(); 46 47 // Legacy methods — contains() tests by VALUE (not key!) 48 System.out.println("=== Legacy methods ==="); 49 System.out.println("contains(50) : " + productStock.contains(50)); // true — value exists 50 System.out.println("containsKey(Mouse): " + productStock.containsKey("Mouse")); // true — key exists 51 System.out.println("containsValue(25): " + productStock.containsValue(25)); // true 52 53 // keys() and elements() — Enumeration (legacy) 54 System.out.println(); 55 System.out.println("=== Enumeration (legacy iteration) ==="); 56 Enumeration<String> keyEnum = productStock.keys(); 57 System.out.print("keys() : "); 58 while (keyEnum.hasMoreElements()) { 59 System.out.print(keyEnum.nextElement() + " "); 60 } 61 System.out.println(); 62 63 Enumeration<Integer> valEnum = productStock.elements(); 64 System.out.print("elements(): "); 65 while (valEnum.hasMoreElements()) { 66 System.out.print(valEnum.nextElement() + " "); 67 } 68 System.out.println(); 69 70 System.out.println(); 71 72 // Modern iteration — forEach (Java 8+, works on Hashtable) 73 System.out.println("=== Modern forEach iteration ==="); 74 productStock.forEach((product, stock) -> 75 System.out.printf(" %-12s stock=%d%n", product, stock)); 76 } 77}
Output:
=== Map interface methods (all synchronised) ===
Hashtable   : {Webcam=63, Keyboard=87, Laptop=42, Mouse=150, Monitor=25}
size()      : 5
get(Laptop) : 42
containsKey(Mouse) : true
get(Tablet) : null

Updated Laptop stock, old value was: 42
New Laptop stock: 50

=== Null rejection (both key and value) ===
put(null key) throws NullPointerException
put(null value) throws NullPointerException

=== Legacy methods ===
contains(50)     : true
containsKey(Mouse): true
containsValue(25): true

=== Enumeration (legacy iteration) ===
keys()    : Webcam Keyboard Laptop Mouse Monitor
elements(): 63 87 50 150 25

=== Modern forEach iteration ===
  Webcam       stock=63
  Keyboard     stock=87
  Laptop       stock=50
  Mouse        stock=150
  Monitor      stock=25

Hashtable vs HashMap — The Null and Synchronisation Contrast

Java
1// File: HashtableVsHashMapDemo.java 2 3import java.util.HashMap; 4import java.util.Hashtable; 5import java.util.Map; 6 7public class HashtableVsHashMapDemo { 8 9 public static void main(String[] args) { 10 11 // ---- NULL KEY BEHAVIOUR ---- 12 System.out.println("=== NULL KEY DIFFERENCE ==="); 13 Map<String, String> hashMap = new HashMap<>(); 14 hashMap.put(null, "null-key-value"); // HashMap: allowed 15 hashMap.put("key", null); // HashMap: allowed 16 System.out.println("HashMap null key : " + hashMap.get(null)); 17 System.out.println("HashMap null val : " + hashMap.get("key")); 18 19 Hashtable<String, String> hashtable = new Hashtable<>(); 20 try { hashtable.put(null, "v"); } 21 catch (NullPointerException e) { 22 System.out.println("Hashtable null key throws NPE"); 23 } 24 try { hashtable.put("k", null); } 25 catch (NullPointerException e) { 26 System.out.println("Hashtable null val throws NPE"); 27 } 28 29 System.out.println(); 30 31 // ---- PERFORMANCE — single-threaded lock overhead ---- 32 System.out.println("=== SINGLE-THREADED PERFORMANCE (500,000 puts) ==="); 33 final int N = 500_000; 34 35 Hashtable<Integer, Integer> ht = new Hashtable<>(); 36 long start = System.nanoTime(); 37 for (int i = 0; i < N; i++) ht.put(i, i); 38 long htTime = System.nanoTime() - start; 39 40 Map<Integer, Integer> hm = new HashMap<>(); 41 start = System.nanoTime(); 42 for (int i = 0; i < N; i++) hm.put(i, i); 43 long hmTime = System.nanoTime() - start; 44 45 System.out.printf("Hashtable time: %,d ms%n", htTime / 1_000_000); 46 System.out.printf("HashMap time: %,d ms%n", hmTime / 1_000_000); 47 System.out.printf("Hashtable overhead: ~%.1fx (lock per put)%n", 48 (double) htTime / hmTime); 49 50 System.out.println(); 51 52 // ---- CAPACITY — different starting sizes ---- 53 System.out.println("=== INITIAL CAPACITY DIFFERENCE ==="); 54 Hashtable<String, String> ht2 = new Hashtable<>(); 55 HashMap<String, String> hm2 = new HashMap<>(); 56 // Hashtable default: 11 HashMap default: 16 57 // Hashtable growth: 2n+1 HashMap growth: 2n (always power of 2) 58 System.out.println("Hashtable default capacity: 11 (kept odd for modulo distribution)"); 59 System.out.println("HashMap default capacity: 16 (power of 2 for bitwise AND)"); 60 } 61}
Output:
=== NULL KEY DIFFERENCE ===
HashMap null key : null-key-value
HashMap null val : null
Hashtable null key throws NPE
Hashtable null val throws NPE

=== SINGLE-THREADED PERFORMANCE (500,000 puts) ===
Hashtable  time: 92 ms
HashMap    time: 29 ms
Hashtable overhead: ~3.2x (lock per put)

=== INITIAL CAPACITY DIFFERENCE ===
Hashtable default capacity: 11 (kept odd for modulo distribution)
HashMap   default capacity: 16 (power of 2 for bitwise AND)

Hashtable vs ConcurrentHashMap — The Concurrency Gap

Java
1// File: HashtableVsConcurrentHashMapDemo.java 2 3import java.util.Hashtable; 4import java.util.concurrent.ConcurrentHashMap; 5 6public class HashtableVsConcurrentHashMapDemo { 7 8 public static void main(String[] args) throws InterruptedException { 9 10 System.out.println("=== Compound operation race with Hashtable ==="); 11 12 Hashtable<String, String> ht = new Hashtable<>(); 13 ht.put("session-001", "user-priya"); 14 15 // WRONG pattern — race condition even with Hashtable 16 // Thread 1: containsKey → true, then another thread removes, then put 17 // Result: may overwrite or create duplicates 18 System.out.println("containsKey + put is NOT atomic on Hashtable"); 19 System.out.println("External synchronized(ht) block required"); 20 21 System.out.println(); 22 23 System.out.println("=== ConcurrentHashMap — atomic compound operations ==="); 24 ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>(); 25 chm.put("session-001", "user-priya"); 26 27 // putIfAbsent — single atomic operation 28 String prev = chm.putIfAbsent("session-001", "user-new"); 29 System.out.println("putIfAbsent result (key exists): " + prev); // user-priya 30 31 String prev2 = chm.putIfAbsent("session-002", "user-rohan"); 32 System.out.println("putIfAbsent result (key absent): " + prev2); // null = inserted 33 34 // computeIfAbsent — create lazily and atomically 35 chm.computeIfAbsent("session-003", k -> "user-ananya"); 36 System.out.println("After computeIfAbsent: " + chm.get("session-003")); 37 38 // merge — atomic counter increment 39 ConcurrentHashMap<String, Integer> counters = new ConcurrentHashMap<>(); 40 String[] events = {"LOGIN","VIEW","LOGIN","PURCHASE","VIEW","LOGIN"}; 41 for (String event : events) { 42 counters.merge(event, 1, Integer::sum); // atomic — no external lock needed 43 } 44 System.out.println("Event counts: " + counters); 45 46 System.out.println(); 47 48 System.out.println("=== Key differences — Hashtable vs ConcurrentHashMap ==="); 49 System.out.println("Hashtable:"); 50 System.out.println(" - Global lock on every method (reads AND writes)"); 51 System.out.println(" - 100 readers → 99 wait at a time"); 52 System.out.println(" - No atomic compound operations"); 53 System.out.println(" - Null keys/values rejected"); 54 System.out.println(" - Enumeration iteration (legacy)"); 55 System.out.println(); 56 System.out.println("ConcurrentHashMap:"); 57 System.out.println(" - Lock-free reads (volatile fields)"); 58 System.out.println(" - Per-bucket sync for writes"); 59 System.out.println(" - Atomic: putIfAbsent, computeIfAbsent, merge, replace"); 60 System.out.println(" - Null keys/values rejected (same as Hashtable)"); 61 System.out.println(" - Weakly consistent iterator (never CME)"); 62 } 63}
Output:
=== Compound operation race with Hashtable ===
containsKey + put is NOT atomic on Hashtable
External synchronized(ht) block required

=== ConcurrentHashMap — atomic compound operations ===
putIfAbsent result (key exists): user-priya
putIfAbsent result (key absent): null
After computeIfAbsent: user-ananya
Event counts: {PURCHASE=1, LOGIN=3, VIEW=2}

=== Key differences — Hashtable vs ConcurrentHashMap ===
Hashtable:
  - Global lock on every method (reads AND writes)
  - 100 readers → 99 wait at a time
  - No atomic compound operations
  - Null keys/values rejected
  - Enumeration iteration (legacy)

ConcurrentHashMap:
  - Lock-free reads (volatile fields)
  - Per-bucket sync for writes
  - Atomic: putIfAbsent, computeIfAbsent, merge, replace
  - Null keys/values rejected (same as Hashtable)
  - Weakly consistent iterator (never CME)

Real-World Example — Migrating Legacy Servlet Session Cache

A legacy Java EE servlet used Hashtable for session management — a common pattern before ConcurrentHashMap existed. The migration replaces global-lock serialisation with lock-free reads, adds atomic compound operations that eliminate race conditions in putIfAbsent patterns, and switches from Enumeration to modern forEach iteration.

Java
1// File: LegacySessionCache.java (BEFORE — do NOT use in new code) 2 3import java.util.Enumeration; 4import java.util.Hashtable; 5 6public class LegacySessionCache { 7 8 // LEGACY: Hashtable for session storage — every op holds global lock 9 private static final Hashtable<String, String> sessions = new Hashtable<>(); 10 11 public static void storeSession(String token, String userId) { 12 sessions.put(token, userId); // legacy put 13 } 14 15 public static String getSession(String token) { 16 return sessions.get(token); // locked read 17 } 18 19 public static void invalidateSession(String token) { 20 sessions.remove(token); // locked remove 21 } 22 23 public static void printAllSessions() { 24 Enumeration<String> tokens = sessions.keys(); // legacy Enumeration 25 while (tokens.hasMoreElements()) { 26 String token = tokens.nextElement(); 27 System.out.println(" " + token + " → " + sessions.get(token)); 28 } 29 } 30}
Java
1// File: ModernSessionCache.java (AFTER — ConcurrentHashMap migration) 2 3import java.util.Map; 4import java.util.concurrent.ConcurrentHashMap; 5 6public class ModernSessionCache { 7 8 // MODERN: ConcurrentHashMap — lock-free reads, bucket-level writes 9 private static final Map<String, String> sessions = new ConcurrentHashMap<>(); 10 11 // Migration: Hashtable.put() → ConcurrentHashMap.put() (same signature) 12 public static void storeSession(String token, String userId) { 13 sessions.put(token, userId); 14 } 15 16 // Migration: Hashtable.get() → ConcurrentHashMap.get() (same signature) 17 public static String getSession(String token) { 18 return sessions.get(token); 19 } 20 21 // NEW: atomic putIfAbsent — safe registration without race condition 22 // Old pattern: if(!sessions.containsKey(token)) sessions.put(token, userId) 23 // New pattern: single atomic operation 24 public static boolean registerIfNew(String token, String userId) { 25 return sessions.putIfAbsent(token, userId) == null; 26 } 27 28 // NEW: atomic replace — update only if current value matches 29 public static boolean refreshSession(String token, String oldUser, String newUser) { 30 return sessions.replace(token, oldUser, newUser); 31 } 32 33 // Migration: Enumeration → forEach (Java 8+) 34 public static void printAllSessions() { 35 sessions.forEach((token, userId) -> 36 System.out.println(" " + token + " → " + userId)); 37 } 38 39 // Migration: Hashtable.size() → ConcurrentHashMap.mappingCount() for large maps 40 public static long sessionCount() { 41 return sessions.mappingCount(); // returns long — safer for very large maps 42 } 43 44 public static Map<String, String> getReadOnlyView() { 45 return java.util.Collections.unmodifiableMap(sessions); 46 } 47 48 public static void main(String[] args) { 49 50 System.out.println("--- BEFORE: LegacySessionCache (Hashtable) ---"); 51 LegacySessionCache.storeSession("tok-abc123", "user-priya"); 52 LegacySessionCache.storeSession("tok-def456", "user-rohan"); 53 LegacySessionCache.storeSession("tok-ghi789", "user-ananya"); 54 LegacySessionCache.printAllSessions(); 55 56 System.out.println(); 57 58 System.out.println("--- AFTER: ModernSessionCache (ConcurrentHashMap) ---"); 59 ModernSessionCache.storeSession("tok-abc123", "user-priya"); 60 ModernSessionCache.storeSession("tok-def456", "user-rohan"); 61 ModernSessionCache.storeSession("tok-ghi789", "user-ananya"); 62 63 // Atomic putIfAbsent — safe even under concurrent access 64 boolean registered = ModernSessionCache.registerIfNew("tok-abc123", "user-new"); 65 System.out.println("registerIfNew(tok-abc123): " + registered + " (false = already exists)"); 66 67 boolean fresh = ModernSessionCache.registerIfNew("tok-jkl012", "user-karan"); 68 System.out.println("registerIfNew(tok-jkl012): " + fresh + " (true = newly registered)"); 69 70 System.out.println("\nAll sessions:"); 71 ModernSessionCache.printAllSessions(); 72 73 System.out.println("\nSession count: " + ModernSessionCache.sessionCount()); 74 75 System.out.println(); 76 System.out.println("=== MIGRATION MAPPING ==="); 77 System.out.println(" Hashtable.put(k,v) → ConcurrentHashMap.put(k,v)"); 78 System.out.println(" Hashtable.get(k) → ConcurrentHashMap.get(k)"); 79 System.out.println(" Hashtable.remove(k) → ConcurrentHashMap.remove(k)"); 80 System.out.println(" Hashtable.containsKey(k) → ConcurrentHashMap.containsKey(k)"); 81 System.out.println(" Hashtable.contains(v) → ConcurrentHashMap.containsValue(v)"); 82 System.out.println(" Hashtable.keys() Enumeration → ConcurrentHashMap.keySet() or forEach()"); 83 System.out.println(" Hashtable.elements() Enumeration→ ConcurrentHashMap.values() or forEach()"); 84 System.out.println(" Manual if(!ht.contains(k)) put → ConcurrentHashMap.putIfAbsent()"); 85 System.out.println(" Hashtable.size() → ConcurrentHashMap.mappingCount()"); 86 } 87}
Output:
--- BEFORE: LegacySessionCache (Hashtable) ---
  tok-abc123 → user-priya
  tok-def456 → user-rohan
  tok-ghi789 → user-ananya

--- AFTER: ModernSessionCache (ConcurrentHashMap) ---
registerIfNew(tok-abc123): false (false = already exists)
registerIfNew(tok-jkl012): true (true = newly registered)

All sessions:
  tok-abc123 → user-priya
  tok-def456 → user-rohan
  tok-ghi789 → user-ananya
  tok-jkl012 → user-karan

Session count: 4

=== MIGRATION MAPPING ===
  Hashtable.put(k,v)              → ConcurrentHashMap.put(k,v)
  Hashtable.get(k)                → ConcurrentHashMap.get(k)
  Hashtable.remove(k)             → ConcurrentHashMap.remove(k)
  Hashtable.containsKey(k)        → ConcurrentHashMap.containsKey(k)
  Hashtable.contains(v)           → ConcurrentHashMap.containsValue(v)
  Hashtable.keys() Enumeration    → ConcurrentHashMap.keySet() or forEach()
  Hashtable.elements() Enumeration→ ConcurrentHashMap.values() or forEach()
  Manual if(!ht.contains(k)) put  → ConcurrentHashMap.putIfAbsent()
  Hashtable.size()                → ConcurrentHashMap.mappingCount()

Performance Considerations

OperationHashtableHashMapConcurrentHashMap
put(k, v)O(1) avg + global lockO(1) avgO(1) avg + bucket lock
get(k)O(1) avg + global lockO(1) avgO(1) avg, lock-free
remove(k)O(1) avg + global lockO(1) avgO(1) avg + bucket lock
size()O(1) + global lockO(1) no lockApproximate (LongAdder)
Concurrent readsSerialisedData raceFully parallel
Concurrent writesSerialisedData raceBucket-independent
Null keysRejected1 allowedRejected
Null valuesRejectedUnlimitedRejected
Initial capacity111616
Growth×2+1 (odd)×2 (power of 2)×2 (power of 2)
IterationEnumeration + IteratorIterator (fail-fast)Weakly consistent
Compound opsNoNoYes (atomic)

Hashtable.size() holds the global lock. Every size() call acquires the object monitor. This means a long-running put() blocks a concurrent size(). In HashMap, size() reads a plain int field with no lock — instantaneous. In ConcurrentHashMap, size() sums LongAdder cells — approximate but non-blocking.

Hashtable's modulo-based bucket indexing is slower than HashMap's bitwise AND. (hash & 0x7FFFFFFF) % table.length involves a modulo operation per lookup. HashMap's hash & (capacity - 1) is a single bitwise AND instruction. With millions of lookups per second, this constant-factor difference accumulates.

Best Practices

Treat every Hashtable field in existing code as a migration target. When touching a class that contains a Hashtable field, replace it as part of the change. Hashtable<K, V>HashMap<K, V> for single-threaded code. Hashtable<K, V>ConcurrentHashMap<K, V> for concurrent code. The API is almost identical — most put(), get(), remove(), and containsKey() calls work unchanged.

Use java.util.Properties when .properties file parsing is needed — it is the only acceptable Hashtable subclass in modern code. Properties extends Hashtable and is used for loading .properties configuration files. The Properties-specific API (load(), getProperty(), setProperty(), store()) is still current. Use Properties for its purpose-built config API; avoid treating it as a general Hashtable.

Replace Hashtable.contains(value) with containsValue(value) immediately when reading legacy code. contains(value) tests by VALUE — not by key. This is the most confusing legacy method: most Java developers read map.contains(key) and assume it tests for key existence. In Hashtable, it tests for value existence. containsValue() is explicit and works on all Map implementations. containsKey() is the key-existence test.

When migrating compound containsKey() + put() patterns, use ConcurrentHashMap.putIfAbsent(). The legacy pattern if (!ht.containsKey(k)) ht.put(k, v) is a race condition even with Hashtable — two threads can both pass the check. ConcurrentHashMap.putIfAbsent(k, v) is a single atomic operation. Similarly, computeIfAbsent(), merge(), and replace() atomically handle all compound patterns.

Common Mistakes

Mistake 1 — Confusing contains() With containsKey()

Java
1Hashtable<String, Integer> scores = new Hashtable<>(); 2scores.put("Alice", 95); 3scores.put("Bob", 88); 4 5// WRONG — contains() tests by VALUE, not by KEY 6// Many developers assume this tests for the KEY "Alice" 7boolean result = scores.contains("Alice"); // FALSE — "Alice" is a KEY, not a VALUE 8System.out.println("contains(Alice): " + result); // false! 9 10// CORRECT — use containsKey() to check for key existence 11System.out.println("containsKey(Alice): " + scores.containsKey("Alice")); // true 12 13// CORRECT — use containsValue() or contains() only for value checks 14System.out.println("containsValue(95) : " + scores.containsValue(95)); // true 15System.out.println("contains(95) : " + scores.contains(95)); // true — same as containsValue
Output:
contains(Alice): false
containsKey(Alice): true
containsValue(95)  : true
contains(95)       : true

Mistake 2 — Assuming Compound Operations Are Thread-Safe

Java
1Hashtable<String, String> tokenRegistry = new Hashtable<>(); 2 3// WRONG — race condition: two threads may both pass the contains check 4// and both execute put() — one "putIfAbsent" intent becomes a plain overwrite 5if (!tokenRegistry.containsKey("user-001")) { // lock acquired, then RELEASED 6 tokenRegistry.put("user-001", "session-X"); // race window here 7} 8 9// CORRECT — use ConcurrentHashMap.putIfAbsent() for atomic check-then-insert 10java.util.concurrent.ConcurrentHashMap<String, String> safe = new java.util.concurrent.ConcurrentHashMap<>(); 11String previous = safe.putIfAbsent("user-001", "session-X"); 12if (previous == null) { 13 System.out.println("Registered: session-X"); // only one thread succeeds 14}

Mistake 3 — Using Hashtable for Single-Threaded Code

Java
1// WRONG — Hashtable acquires a lock on every operation even with a single thread 2// 3x overhead compared to HashMap for no concurrency benefit 3private Hashtable<String, UserProfile> userCache = new Hashtable<>(); 4 5// CORRECT — HashMap for single-threaded service code 6private Map<String, UserProfile> userCache = new HashMap<>(); 7 8// The lock acquisition and release in Hashtable is wasted work 9// when only one thread ever accesses the object.

Mistake 4 — Iterating Hashtable with Enumeration Instead of Modern Alternatives

Java
1Hashtable<String, Double> prices = new Hashtable<>(); 2prices.put("Rice", 45.0); 3prices.put("Wheat", 38.0); 4prices.put("Sugar", 55.0); 5 6// WRONG — Enumeration lacks remove() and is verbose 7Enumeration<String> keys = prices.keys(); 8while (keys.hasMoreElements()) { 9 String key = keys.nextElement(); 10 System.out.println(key + " → " + prices.get(key)); 11} 12 13// CORRECT — forEach is available on Hashtable (added via Map interface, Java 8+) 14prices.forEach((item, price) -> System.out.println(item + " → " + price)); 15 16// CORRECT — entrySet() for iteration with both key and value in one step 17for (java.util.Map.Entry<String, Double> entry : prices.entrySet()) { 18 System.out.println(entry.getKey() + " → " + entry.getValue()); 19}

Interview Questions

Q1. What is Hashtable in Java and why is it considered legacy?

Hashtable<K, V> is a Map implementation from Java 1.0 that stores key-value pairs in a hash table with separate chaining. Every public method is declared synchronized, so all operations acquire a single global lock on the Hashtable instance. It is considered legacy for four reasons: global locking serialises all readers and writers, wasting CPU time under concurrent read workloads; compound operations like check-then-insert are still race conditions despite per-method synchronisation; null keys and values are rejected, limiting usefulness; and its legacy API (keys(), elements(), contains()) predates the standard Map interface. HashMap replaces it for single-threaded use, ConcurrentHashMap for concurrent use.

Q2. What is the difference between Hashtable and HashMap?

Hashtable synchronises every method with a global lock — all reads and writes serialise. HashMap has no synchronisation — faster in single-threaded code, unsafe if shared between threads. Hashtable rejects null keys and null values. HashMap allows one null key and unlimited null values. Hashtable uses initial capacity 11 and grows by 2n+1; HashMap uses initial capacity 16 and grows to the next power of 2. Hashtable extends Dictionary<K,V> (an obsolete abstract class); HashMap extends AbstractMap<K,V>. Hashtable provides keys() and elements() returning Enumeration; HashMap only provides Iterator-based views. Both use hash tables with separate chaining internally.

Q3. What is the difference between Hashtable and ConcurrentHashMap?

Both are thread-safe maps that reject null keys and values. Hashtable acquires a single global lock on every method call — reads and writes all compete for the same mutex, serialising all concurrent access. ConcurrentHashMap uses lock-free reads via volatile fields and per-bucket synchronisation for writes — multiple threads can read simultaneously, and multiple writers can write to different buckets without contention. ConcurrentHashMap also provides atomic compound operations — putIfAbsent(), computeIfAbsent(), merge(), replace() — that Hashtable cannot offer without external locking. For any new concurrent map requirement, ConcurrentHashMap is the correct choice.

Q4. Why does Hashtable reject null keys and values?

Hashtable's put() implementation calls key.hashCode() and checks value == null before storing. Both checks throw NullPointerException for null inputs — this is an explicit design decision, not an accidental omission. The get() method returns null to signal "key not found." If null values were allowed, get(key) == null would be ambiguous: it could mean key not found OR key maps to null — the same ambiguity that makes ConcurrentHashMap also reject null. HashMap allows null key (stored in bucket 0 via special handling) and null values (distinguished from "key not found" only via containsKey()).

Q5. What does Hashtable.contains() do — and why is it confusing?

Hashtable.contains(value) tests whether any key maps to the given value — it searches by VALUE, not by key. This is the opposite of what most developers expect. map.contains("Alice") on a Hashtable returns false if "Alice" is only a key, not a value. The correct key-existence check is containsKey("Alice"). The contains() method is equivalent to containsValue() — it was introduced before the Map interface existed and the naming convention was not established. In code reviews, Hashtable.contains() should always be replaced with the explicit containsKey() or containsValue().

Q6. Is java.util.Properties a Hashtable?

Yes. java.util.Properties extends Hashtable<Object, Object>. It is the most widely used surviving subclass of Hashtable. Properties provides the getProperty(key) and setProperty(key, value) methods on top of Hashtable operations, and load() / store() for reading and writing .properties files. Because it extends Hashtable, it inherits all global synchronisation and null rejection. Properties is actively used and not deprecated — it is the correct tool for loading application configuration from .properties files. The problem is treating Properties as a general-purpose thread-safe Map, which is the same limitation as using Hashtable directly.

FAQs

Should I ever use Hashtable in new Java code?

No — with one exception: java.util.Properties extends Hashtable and is the standard API for loading .properties configuration files. Its purpose-built getProperty() / load() API is still appropriate for that use case. For general key-value storage, use HashMap for single-threaded code and ConcurrentHashMap for concurrent code.

Is Hashtable deprecated in Java?

Hashtable has never been formally annotated with @Deprecated. It is informally considered legacy — the Javadoc recommends HashMap for non-synchronised use and ConcurrentHashMap for synchronised use. Like Vector, it remains fully functional for backward compatibility. IDE inspection tools (IntelliJ, SonarQube) flag Hashtable usage with a warning but do not fail compilation.

Why does Hashtable use an odd initial capacity of 11?

The modulo operation hash % capacity produces better distribution when capacity is an odd number — particularly a prime. An even capacity produces patterns where many hash codes cluster into even-numbered buckets. Using 11 (a prime) as the default was a deliberate optimisation for the modulo-based indexing Hashtable uses. HashMap's power-of-2 approach combined with XOR spreading of high bits achieves better distribution without relying on odd capacity, and uses the faster bitwise AND operation instead of modulo.

Can I use Hashtable with Java 8 Stream API?

Yes. Hashtable.entrySet(), keySet(), and values() all return Collection views, and Collection.stream() is available since Java 8. hashtable.entrySet().stream().filter(...).map(...).collect(...) works correctly. hashtable.forEach(BiConsumer) also works as a Java 8 default method. The legacy keys() and elements() returning Enumeration can be converted: Collections.list(hashtable.keys()).stream().

What is the Enumeration interface and how does it differ from Iterator?

Enumeration<E> is the Java 1.0 predecessor to Iterator<E>. It has two methods: hasMoreElements() (equivalent to Iterator.hasNext()) and nextElement() (equivalent to Iterator.next()). Enumeration lacks the remove() method — elements cannot be removed during traversal. It also does not support fail-fast detection or the enhanced for-each loop directly (you need Collections.list() to convert it first). All modern Java code uses Iterator or for-each; Enumeration remains only in legacy APIs.

What is the relationship between Hashtable and java.util.Properties?

java.util.Properties directly extends Hashtable<Object, Object>. It inherits all of Hashtable's synchronisation, null rejection, and legacy Enumeration API. Properties adds a purpose-built API for string key-value configuration: getProperty(key), setProperty(key, value), load(Reader), store(Writer, comment), and loadFromXML(). Properties is the only Hashtable subclass still actively used and recommended in modern Java — specifically for reading .properties configuration files. All other Hashtable usage should be migrated to HashMap or ConcurrentHashMap.

Summary

Hashtable<K, V> is Java's original synchronised key-value store — a hash table where every method acquires a global lock before executing. It solved the thread-safety problem that existed before the java.util.concurrent package, at the cost of serialising every read and write through the same mutex. Null keys and null values are rejected. contains() tests by value, not key. The legacy keys() and elements() methods return Enumeration.

For interviews: the most important comparisons are Hashtable vs HashMap (synchronised vs not, null policy, growth strategy, capacity) and Hashtable vs ConcurrentHashMap (global lock vs lock-free reads, no atomic ops vs full atomic API). Both comparisons collapse to the same conclusion: Hashtable is obsolete, and ConcurrentHashMap is the correct modern replacement for thread-safe map use.

The one surviving subclass worth knowing is java.util.Properties — it extends Hashtable and remains the standard for .properties configuration files. Everything else that was once built on Hashtable should be migrated.

What to Read Next

TopicLink
How HashMap provides the same hash table without synchronisation overheadJava HashMap →
How ConcurrentHashMap replaces Hashtable with lock-free reads and atomic operationsJava HashSet →
How the Collections Framework organises all Map implementations from legacy to modernJava Collections Framework →
How Generics retrofitted type safety onto Hashtable's originally raw Object storageJava Generics →
How the Iterator interface replaced Enumeration as the standard traversal APIJava Iterator →
Java Hashtable | DevStackFlow