☕ Java

Java Hashtable

A complete guide to Java Hashtable — internal bucket-array hashing mechanism, all Map methods, load factor and rehashing, thread-safety model, Hashtable vs HashMap vs ConcurrentHashMap comparison, legacy context, and best practices with real-world examples.

📅

Last Updated

March 2026

⏱️

Read Time

17 min

🎯

Level

Beginner to Intermediate

What is Java Hashtable?

Java Hashtable is one of the oldest data structures in the Java standard library — it has existed since Java 1.0, predating the entire Collections Framework. It implements the Map<K, V> interface and stores data as key-value pairs inside an internal array of buckets, using a hash function to determine where each entry is placed.

The defining characteristic of Hashtable is that every public method is synchronized, making it inherently thread-safe. This was the primary reason for its existence in Java's early days when thread-safe maps were a necessity and no better alternative existed. However, this coarse-grained locking strategy — locking the entire table for every operation — makes it significantly slower than modern alternatives in multi-threaded environments.

Hashtable has two firm rules that distinguish it from HashMap: it does not allow null keys and it does not allow null values. Attempting to insert either immediately throws a NullPointerException. It also does not maintain insertion order, and iteration order is not guaranteed to be consistent across JVM versions.

Today, Hashtable is considered a legacy class. The Java documentation itself recommends using HashMap for non-concurrent scenarios and ConcurrentHashMap for thread-safe scenarios. Understanding Hashtable remains important for maintaining legacy codebases and for Java interviews, where its differences from HashMap are among the most commonly tested topics.

Internal Structure — How Hashtable Works Under the Hood

Hashtable internally uses an array of Entry (bucket) objects. Each bucket is the head of a singly linked list (chain) of entries that share the same hash bucket — this technique for resolving collisions is called separate chaining.

☕ JavaHashtable Internal Entry Structure (Simplified from JDK Source)
// Simplified representation of Hashtable's internal Entry
private static class Entry<K, V> implements Map.Entry<K, V> {
    final int   hash;   // cached hash of the key
    final K     key;    // the key — never null
    V           value;  // the value — never null
    Entry<K,V>  next;   // next entry in the same bucket (chaining)
}

// Core internal state of Hashtable:
private transient Entry<?,?>[] table;   // the bucket array
private transient int count;             // total number of entries
private int threshold;                   // count at which rehash occurs (capacity × loadFactor)
private float loadFactor;                // default 0.75f

// Default construction: capacity=11, loadFactor=0.75
// threshold = 11 × 0.75 = 8  →  rehash when 9th entry is added

How put(key, value) works step by step:

📊 DiagramHashtable Internal Bucket Layout — put("name", "Rahul")
  Step 1: Compute hash  →  hash = key.hashCode()  (null key → NullPointerException)
  Step 2: Find bucket   →  index = (hash & 0x7FFFFFFF) % table.length
  Step 3: Check bucket  →  scan linked-list chain for existing key (equals check)
            If key found  →  update value, return old value
            If not found  →  prepend new Entry to chain head
  Step 4: Check threshold →  if ++count > threshold  →  rehash()

  Bucket Array (capacity = 11):
  ┌─────┐
  │  0  │ → null
  │  1  │ → ["city"="Delhi"] → null
  │  2  │ → null
  │  3  │ → ["name"="Rahul"] → ["dept"="Eng"] → null  ← collision chain
  │  4  │ → null
  │  5  │ → ["id"="101"] → null
  │ ... │
  └─────┘

Key difference from HashMap internals: HashMap uses the initial capacity of 16 (power of two) and computes the bucket index using bitwise AND: index = hash & (capacity - 1). Hashtable uses the initial capacity of 11 (prime number) and computes bucket index using modulo: index = (hash & 0x7FFFFFFF) % capacity. The prime-number capacity in Hashtable was historically chosen to reduce clustering, though modern implementations have moved to power-of-two sizing with better hash spreading.

Hashtable Class Hierarchy

Hashtable sits in a unique position in the Java type hierarchy — it extends Dictionary, a legacy abstract class from Java 1.0, while also implementing the modern Map interface added in Java 1.2.

java.lang.Object
Root of all Java classes
java.util.Dictionary<K,V> (Legacy abstract class — Java 1.0)
Obsolete predecessor to Map — defines get(), put(), remove(), keys(), elements()Note: Dictionary is considered obsolete; Map interface replaced it
java.util.Map<K,V> (Modern interface — Java 1.2)
Core map contract: put, get, remove, containsKey, containsValue, keySet, values, entrySet
java.util.Hashtable<K,V>
Concrete class — synchronized bucket-array mapExtends Dictionary + Implements Map, Cloneable, SerializableEvery method is synchronized — coarse table-level locking
java.util.Properties (Subclass of Hashtable)
Specialized Hashtable<Object,Object> for .properties configuration filesload(InputStream), getProperty(key), setProperty(key,value)

Architecture Diagram

Notable subclass: java.util.Properties extends Hashtable and is widely used in Java applications for reading configuration files (.properties). If you have used System.getProperties() or loaded application.properties manually, you have already used a Hashtable subclass.

Creating a Hashtable in Java

Hashtable provides four constructors. Understanding each helps you tune initial capacity and load factor for your specific workload.

☕ JavaCreating Hashtable — All Constructors
import java.util.*;

public class CreateHashtable {
    public static void main(String[] args) {

        // 1. Default constructor: capacity=11, loadFactor=0.75
        Hashtable<String, Integer> ht1 = new Hashtable<>();

        // 2. Specify initial capacity (useful when size is known)
        Hashtable<String, String> ht2 = new Hashtable<>(50);

        // 3. Specify both initial capacity and load factor
        //    loadFactor=0.5 means rehash at 50% fill — fewer collisions, more memory
        Hashtable<String, Double> ht3 = new Hashtable<>(100, 0.5f);

        // 4. Initialize from an existing Map
        Map<String, Integer> source = new HashMap<>();
        source.put("Java",   1);
        source.put("Python", 2);
        Hashtable<String, Integer> ht4 = new Hashtable<>(source);
        System.out.println("From map: " + ht4);

        // Using Map interface reference (recommended)
        Map<String, String> config = new Hashtable<>();
        config.put("host", "localhost");
        config.put("port", "8080");
        System.out.println("Config: " + config);

        // ❌ NullPointerException — null key not allowed
        // ht1.put(null, 1);

        // ❌ NullPointerException — null value not allowed
        // ht1.put("key", null);
    }
}

Output

From map: {Python=2, Java=1} Config: {port=8080, host=localhost}

Hashtable Methods — Complete Reference

Hashtable implements all methods of the Map interface plus several legacy methods inherited from the Dictionary class. Every method listed below is synchronized.

MethodDescriptionReturns / Throws
put(K key, V value)Associates key with value. Overwrites existing value if key already present.Previous value, or null if new key. Throws NPE for null key/value.
get(Object key)Returns value associated with the specified key.Value, or null if key not found. Throws NPE for null key.
remove(Object key)Removes the entry for the specified key.Removed value, or null if key not present.
containsKey(Object key)Returns true if the specified key exists in the table.boolean. Throws NPE for null key.
containsValue(Object value)Returns true if one or more keys map to the specified value.boolean. O(n) — scans all buckets.
contains(Object value)Legacy alias for containsValue() — inherited from Dictionary.boolean. Throws NPE for null value.
size()Returns the total number of key-value entries.int
isEmpty()Returns true if the table contains no entries.boolean
clear()Removes all entries from the table.void
putAll(Map<? extends K,? extends V> m)Copies all entries from the given map into this Hashtable.void. Throws NPE if any key or value in source map is null.
getOrDefault(Object key, V defaultValue)Returns value for key, or defaultValue if key is absent.V (Java 8+)
putIfAbsent(K key, V value)Inserts entry only if key is not already present.Existing value if present, null if newly inserted (Java 8+). Throws NPE for null.
replace(K key, V value)Replaces entry only if key currently maps to any value.Previous value, or null if not present (Java 8+).
replace(K key, V oldValue, V newValue)Replaces entry only if key currently maps to oldValue.boolean (Java 8+).
remove(Object key, Object value)Removes entry only if key maps to the specified value.boolean (Java 8+).
keySet()Returns a Set view of all keys in the table.Set<K> — backed by the Hashtable.
values()Returns a Collection view of all values in the table.Collection<V> — backed by the Hashtable.
entrySet()Returns a Set view of all key-value Map.Entry pairs.Set<Map.Entry<K,V>> — backed by the Hashtable.
keys()Legacy method — returns an Enumeration of keys.Enumeration<K> — use keySet() instead.
elements()Legacy method — returns an Enumeration of values.Enumeration<V> — use values() instead.
forEach(BiConsumer)Performs the given action for each entry (Java 8+).void
compute(K key, BiFunction)Computes new value for key using the given function (Java 8+).New computed value
computeIfAbsent(K key, Function)Computes and inserts value only if key is absent (Java 8+).Existing or computed value
computeIfPresent(K key, BiFunction)Computes and replaces value only if key is present (Java 8+).New value or null
merge(K key, V value, BiFunction)Merges a new value with existing value using the given function (Java 8+).Merged value
clone()Returns a shallow copy of the Hashtable.Object — same entries, different Hashtable instance.
toString()Returns a string representation: {key1=value1, key2=value2, ...}String
☕ JavaHashtable Methods — Hands-On Example
import java.util.*;

public class HashtableMethods {
    public static void main(String[] args) {

        Hashtable<String, Integer> scores = new Hashtable<>();

        // put() — add entries
        scores.put("Alice",  92);
        scores.put("Bob",    78);
        scores.put("Charlie",85);
        scores.put("Diana",  95);
        System.out.println("Scores: " + scores);

        // get()
        System.out.println("Alice's score: " + scores.get("Alice"));

        // containsKey() and containsValue()
        System.out.println("Has Bob: "    + scores.containsKey("Bob"));
        System.out.println("Score 85 exists: " + scores.containsValue(85));

        // getOrDefault() — Java 8
        System.out.println("Eve's score: " + scores.getOrDefault("Eve", 0));

        // putIfAbsent() — Java 8
        scores.putIfAbsent("Bob", 99);  // Bob already exists — no change
        scores.putIfAbsent("Eve", 88);  // Eve is new — inserted
        System.out.println("After putIfAbsent: " + scores);

        // replace()
        scores.replace("Charlie", 90);
        System.out.println("After replace Charlie: " + scores);

        // remove()
        scores.remove("Diana");
        System.out.println("After remove Diana: " + scores);

        // size() and isEmpty()
        System.out.println("Size: " + scores.size());
        System.out.println("Empty: " + scores.isEmpty());

        // forEach — Java 8
        System.out.println("--- All Scores ---");
        scores.forEach((name, score) ->
            System.out.println(name + " → " + score));
    }
}

Output

Scores: {Diana=95, Alice=92, Charlie=85, Bob=78} Alice's score: 92 Has Bob: true Score 85 exists: true Eve's score: 0 After putIfAbsent: {Eve=88, Diana=95, Alice=92, Charlie=85, Bob=78} After replace Charlie: {Eve=88, Diana=95, Alice=92, Charlie=90, Bob=78} After remove Diana: {Eve=88, Alice=92, Charlie=90, Bob=78} Size: 4 Empty: false --- All Scores --- Eve → 88 Alice → 92 Charlie → 90 Bob → 78

Load Factor and Rehashing in Hashtable

Two of the most important performance-tuning concepts in Hashtable are load factor and rehashing. Understanding them helps you configure Hashtable (and its modern successor HashMap) for optimal performance.

  • 📐 Load Factor — A float value between 0.0 and 1.0 that controls the fill-threshold of the bucket array. Default is 0.75f. When the number of entries exceeds capacity × loadFactor, the table is rehashed. Formula: threshold = capacity × loadFactor. With default values: threshold = 11 × 0.75 = 8. The 9th insertion triggers rehashing.

  • 🔄 Rehashing — When the threshold is exceeded, Hashtable creates a new bucket array with capacity approximately 2× the old capacity + 1 (e.g., 11 → 23 → 47 → ...). Every existing entry is re-inserted into the new array using the new modulo calculation. Rehashing is O(n) and synchronized — a full table lock is held for the entire duration, which is a major concurrency bottleneck.

  • 📉 Low Load Factor (e.g., 0.25) — Fewer collisions, faster lookups — but higher memory usage because the table is kept mostly empty. Rehashing occurs more frequently.

  • 📈 High Load Factor (e.g., 0.95) — Memory-efficient — table stays dense — but more collisions occur. Lookup degrades as chains grow longer. Rehashing occurs rarely.

  • ✅ Optimal Tuning — The default load factor of 0.75 is carefully chosen to balance memory and performance. Only deviate if you have profiling data showing it is a bottleneck. If final table size is known in advance, set initialCapacity = expectedSize / loadFactor + 1 to avoid any rehashing at all.

☕ JavaLoad Factor Tuning Example
import java.util.*;

public class LoadFactorDemo {
    public static void main(String[] args) {

        // Expected to store ~100 entries
        // Set initial capacity to avoid any rehashing:
        // initialCapacity = expectedSize / loadFactor + 1 = 100 / 0.75 + 1 ≈ 135
        Hashtable<String, String> ht = new Hashtable<>(135, 0.75f);

        // Now inserting 100 entries will never trigger rehashing
        for (int i = 1; i <= 100; i++) {
            ht.put("key-" + i, "value-" + i);
        }
        System.out.println("Entries: " + ht.size());  // 100
        System.out.println("No rehashing occurred — table pre-sized correctly.");
    }
}

Output

Entries: 100 No rehashing occurred — table pre-sized correctly.

Hashtable vs HashMap vs ConcurrentHashMap

This is the most important comparison in Java Collections. Knowing when to use each one is essential for writing correct, performant, and maintainable Java code.

FeatureHashtableHashMapConcurrentHashMap
Introduced inJava 1.0 (legacy)Java 1.2 (Collections Framework)Java 1.5 (java.util.concurrent)
Thread-Safe?✅ Yes — all methods synchronized❌ No — not synchronized✅ Yes — segment/bucket-level locking
Locking StrategyCoarse — entire table locked per operationNoneFine-grained — only affected bucket locked
Null Keys Allowed?❌ No — NullPointerException✅ Yes — exactly one null key allowed❌ No — NullPointerException
Null Values Allowed?❌ No — NullPointerException✅ Yes — multiple null values allowed❌ No — NullPointerException
Iteration OrderNo guaranteed orderNo guaranteed orderNo guaranteed order (weakly consistent)
Iterator TypeEnumeration (legacy) + IteratorIterator (fail-fast)Iterator (weakly consistent — no CME)
Performance (Single Thread)Slow — lock overhead even with no contentionFast — zero overheadModerate — small CAS overhead
Performance (Multi-Thread)Poor — serialized access on whole tableUnsafe — data corruption riskExcellent — concurrent reads, partial writes
RehashingFull table lock during rehashSingle-thread onlyPer-segment rehash — minimal blocking
Recommended ForLegacy code only — avoid in new codeSingle-threaded or externally synchronizedAll multi-threaded use cases
ExtendsDictionary (legacy abstract class)AbstractMapAbstractMap
ImplementsMap, Cloneable, SerializableMap, Cloneable, SerializableConcurrentMap, Serializable

Decision Rule: In 2026, there is almost never a reason to use Hashtable in new code. Use HashMap for single-threaded code. Use ConcurrentHashMap for multi-threaded code. The only valid reason to keep Hashtable in a codebase is legacy compatibility or when the existing API contract requires it (e.g., when a Properties subclass is required).

Thread Safety in Hashtable — How It Works and Why It Falls Short

Hashtable achieves thread safety by declaring every public method with the synchronized keyword. This means only one thread can execute any Hashtable operation at a time — even two threads doing concurrent reads must wait for each other.

☕ JavaHashtable Thread Safety — Demonstration
import java.util.*;

public class HashtableThreadSafety {
    public static void main(String[] args) throws InterruptedException {

        Hashtable<String, Integer> ht = new Hashtable<>();
        ht.put("counter", 0);

        // Two threads simultaneously incrementing the counter
        Runnable incrementTask = () -> {
            for (int i = 0; i < 1000; i++) {
                // get and put are individually synchronized —
                // but the compound operation is NOT atomic!
                int current = ht.get("counter");
                ht.put("counter", current + 1);
            }
        };

        Thread t1 = new Thread(incrementTask);
        Thread t2 = new Thread(incrementTask);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // Result is NOT guaranteed to be 2000!
        // Individual get() and put() are synchronized,
        // but the get-then-put compound action is a race condition.
        System.out.println("Final counter: " + ht.get("counter"));
        System.out.println("Expected: 2000 — actual may differ due to race condition.");

        // CORRECT approach for atomic compound operations:
        // Use ConcurrentHashMap with compute() or AtomicInteger
    }
}

Critical insight: Hashtable's method-level synchronization guarantees that no two threads execute the same method simultaneously, but it does not guarantee atomicity of compound operations like check-then-act or read-modify-write patterns. For true thread-safe atomic operations, use ConcurrentHashMap's compute(), merge(), or computeIfAbsent().

Iterating Over a Hashtable

Hashtable supports both legacy Enumeration-based and modern Iterator-based traversal. Always prefer the modern approaches — they are more expressive and integrate with Java 8+ streams.

☕ JavaAll Ways to Iterate a Hashtable
import java.util.*;

public class IterateHashtable {
    public static void main(String[] args) {

        Hashtable<String, String> ht = new Hashtable<>();
        ht.put("IN", "India");
        ht.put("US", "United States");
        ht.put("JP", "Japan");
        ht.put("DE", "Germany");

        // 1. entrySet() — most common, access both key and value
        System.out.println("--- entrySet ---");
        for (Map.Entry<String, String> entry : ht.entrySet()) {
            System.out.println(entry.getKey() + " → " + entry.getValue());
        }

        // 2. keySet() — iterate keys, fetch values
        System.out.println("--- keySet ---");
        for (String code : ht.keySet()) {
            System.out.println(code + " : " + ht.get(code));
        }

        // 3. values() — iterate values only
        System.out.println("--- values ---");
        for (String country : ht.values()) {
            System.out.println(country);
        }

        // 4. forEach with lambda (Java 8+) — cleanest approach
        System.out.println("--- forEach lambda ---");
        ht.forEach((code, country) ->
            System.out.println(code + " = " + country));

        // 5. Legacy Enumeration (keys) — avoid in new code
        System.out.println("--- Legacy Enumeration ---");
        Enumeration<String> keys = ht.keys();
        while (keys.hasMoreElements()) {
            String k = keys.nextElement();
            System.out.println(k + " -> " + ht.get(k));
        }

        // ⚠️ NOTE: Hashtable iterators are fail-fast.
        // Modifying the table during iteration (except via iterator.remove())
        // throws ConcurrentModificationException.
    }
}

Hashtable put() Operation Flow — Flowchart

The flowchart below shows the exact decision path Java follows when put(key, value) is called on a Hashtable.

📝 put(key, value)called on Hashtable
❓ key or value null?
Yes — null found
💥 NullPointerExceptionthrown immediately
🔐 Acquire synchronized lockon entire Hashtable
🔢 Compute bucket index(hash & 0x7FFFFFFF) % capacity
❓ Key exists in bucket chain?scan chain with equals()
Yes — key found
♻️ Update existing valuereturn old value
➕ Prepend new Entryto bucket chain head
❓ count > threshold?capacity × loadFactor exceeded?
Yes — rehash
🔄 rehash()new capacity ≈ 2×old + 1
🔓 Release lockreturn null (new key)

Code Execution Flow — from source to output

Hashtable with Generics and Custom Objects

Hashtable is fully generic and works with custom key and value types. When using a custom class as a key, you must override both hashCode() and equals() — failing to do so breaks key lookup entirely.

☕ JavaCustom Key in Hashtable — hashCode + equals Contract
import java.util.*;

class ProductId {
    private final String category;
    private final int    number;

    ProductId(String category, int number) {
        this.category = category;
        this.number   = number;
    }

    // MUST override equals — Hashtable uses this to find keys in chains
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ProductId)) return false;
        ProductId other = (ProductId) o;
        return number == other.number && category.equals(other.category);
    }

    // MUST override hashCode — determines which bucket the key lands in
    @Override
    public int hashCode() {
        return Objects.hash(category, number);
    }

    @Override
    public String toString() {
        return category + "-" + number;
    }
}

public class HashtableCustomKey {
    public static void main(String[] args) {

        Hashtable<ProductId, String> catalog = new Hashtable<>();
        catalog.put(new ProductId("ELEC", 1001), "Wireless Headphones");
        catalog.put(new ProductId("ELEC", 1002), "Bluetooth Speaker");
        catalog.put(new ProductId("HOME", 2001), "Coffee Maker");

        // Lookup using a NEW ProductId object with same values
        // Works only because equals() and hashCode() are correctly overridden
        String product = catalog.get(new ProductId("ELEC", 1001));
        System.out.println("Found: " + product);

        catalog.forEach((id, name) ->
            System.out.println(id + " => " + name));
    }
}

Output

Found: Wireless Headphones HOME-2001 => Coffee Maker ELEC-1002 => Bluetooth Speaker ELEC-1001 => Wireless Headphones

Best Practices for Using Hashtable

Most best practices for Hashtable center around one theme: knowing when NOT to use it and what to use instead. When you do encounter it in a codebase, follow these guidelines.

  • ✅ 1. Prefer ConcurrentHashMap for Thread-Safe Maps — If you need a synchronized map in new code, always use ConcurrentHashMap. It offers far better concurrency through bucket-level (fine-grained) locking — multiple threads can read and write to different buckets simultaneously, unlike Hashtable which serializes every operation.

  • ✅ 2. Prefer HashMap for Single-Threaded Code — If thread safety is not needed, HashMap is always faster. It has zero synchronization overhead and allows null keys and values.

  • ✅ 3. Use Map Interface Reference — Declare variables as Map<K,V> rather than Hashtable<K,V>. This allows switching to HashMap or ConcurrentHashMap with a one-line change when requirements change.

  • ✅ 4. Pre-Size to Avoid Rehashing — If you know the approximate number of entries, set initialCapacity = expectedSize / 0.75 + 1. Rehashing is O(n) and acquires a full table lock — it is both CPU-expensive and a concurrency bottleneck.

  • ✅ 5. Always Override hashCode() and equals() for Custom Keys — If you use a custom class as a Hashtable key, both methods must be correctly overridden and consistent with each other. A broken hashCode() scatters equal keys across different buckets, making lookups return null even when the key exists.

  • ✅ 6. Use Modern Iteration (forEach / entrySet) — Avoid the legacy keys() and elements() Enumeration methods. Use forEach(BiConsumer), entrySet() iteration, or stream() for cleaner, more readable code.

  • ❌ 7. Do Not Use Hashtable for Compound Operations — get() followed by put() is NOT atomic even in Hashtable. Between the two synchronized calls, another thread can modify the value. Use ConcurrentHashMap.compute(), computeIfAbsent(), or merge() for truly atomic compound operations.

  • ❌ 8. Never Extend Hashtable Unnecessarily — Extending Hashtable to add behavior is an anti-pattern. The only legitimate subclass in the JDK is Properties. Prefer composition over inheritance for adding functionality to a map.

Real-World Code Examples

Example 1 — Session Store (Hashtable as Thread-Safe Cache)

☕ JavaHashtable as a Simple Session Store
import java.util.*;

class SessionStore {

    // Hashtable provides built-in thread safety for concurrent session access
    private final Hashtable<String, String> sessions = new Hashtable<>();

    public void createSession(String sessionId, String username) {
        sessions.put(sessionId, username);
        System.out.println("Session created: " + sessionId + " → " + username);
    }

    public String getUser(String sessionId) {
        return sessions.getOrDefault(sessionId, "ANONYMOUS");
    }

    public void invalidate(String sessionId) {
        String user = sessions.remove(sessionId);
        if (user != null) {
            System.out.println("Session invalidated for: " + user);
        }
    }

    public boolean isActive(String sessionId) {
        return sessions.containsKey(sessionId);
    }

    public int activeSessions() {
        return sessions.size();
    }
}

public class SessionApp {
    public static void main(String[] args) {
        SessionStore store = new SessionStore();

        store.createSession("SID-001", "rahul@example.com");
        store.createSession("SID-002", "priya@example.com");
        store.createSession("SID-003", "amit@example.com");

        System.out.println("User for SID-001: " + store.getUser("SID-001"));
        System.out.println("SID-999 active: "  + store.isActive("SID-999"));
        System.out.println("Active sessions: " + store.activeSessions());

        store.invalidate("SID-002");
        System.out.println("Active sessions after logout: " + store.activeSessions());
    }
}

Output

Session created: SID-001 → rahul@example.com Session created: SID-002 → priya@example.com Session created: SID-003 → amit@example.com User for SID-001: rahul@example.com SID-999 active: false Active sessions: 3 Session invalidated for: priya@example.com Active sessions after logout: 2

Example 2 — Migrating from Hashtable to ConcurrentHashMap

☕ JavaHashtable → ConcurrentHashMap Migration Pattern
import java.util.*;
import java.util.concurrent.*;

public class MigrationDemo {

    // ❌ OLD — Legacy Hashtable (coarse locking, no nulls, poor concurrency)
    static void legacyApproach() {
        Hashtable<String, Integer> inventory = new Hashtable<>();
        inventory.put("Laptop",  50);
        inventory.put("Monitor", 30);
        inventory.put("Keyboard", 100);

        // Check-and-update is NOT atomic — race condition risk
        if (inventory.containsKey("Laptop")) {
            inventory.put("Laptop", inventory.get("Laptop") - 1);
        }
        System.out.println("[Legacy] Laptop stock: " + inventory.get("Laptop"));
    }

    // ✅ NEW — ConcurrentHashMap (fine-grained locking, atomic compound ops)
    static void modernApproach() {
        ConcurrentHashMap<String, Integer> inventory = new ConcurrentHashMap<>();
        inventory.put("Laptop",  50);
        inventory.put("Monitor", 30);
        inventory.put("Keyboard", 100);

        // compute() is fully atomic — no race condition
        inventory.compute("Laptop", (key, qty) -> (qty != null && qty > 0) ? qty - 1 : 0);
        System.out.println("[Modern] Laptop stock: " + inventory.get("Laptop"));
    }

    public static void main(String[] args) {
        legacyApproach();
        modernApproach();
    }
}

Output

[Legacy] Laptop stock: 49 [Modern] Laptop stock: 49

Practice This Code — Live Editor

Advantages and Disadvantages of Hashtable

Hashtable is a battle-tested but outdated data structure. Its advantages were once compelling; today they are largely superseded by better alternatives.

✅ Advantages
Built-in Thread SafetyEvery public method is synchronized — no external synchronization required for basic operations. In Java's early days (pre-1.2), this was the only way to get a thread-safe map without writing your own.
No Null Key/Value — Fail FastRejecting null keys and values immediately with NullPointerException prevents silent null-related bugs that can be hard to diagnose in HashMap-based code.
Properties Subclass Ecosystemjava.util.Properties extends Hashtable and is the foundation for Java's configuration file system. Working with .properties files, System properties, or Maven/Gradle configurations all involve Hashtable indirectly.
Long-Standing StabilityHashtable has been in Java since version 1.0. Its behavior is exhaustively documented and well-understood. In legacy systems, its predictable (if slow) behavior is sometimes preferred over introducing newer dependencies.
Works with Legacy APIsSome older Java APIs and frameworks explicitly expect Hashtable or its subclasses. In those contexts, Hashtable remains the only correct choice without refactoring the API contract.
❌ Disadvantages
Coarse-Grained Locking Kills ConcurrencyThe whole-table lock means only one thread executes any operation at a time. Under high concurrency, threads queue up waiting for the lock — causing severe throughput degradation compared to ConcurrentHashMap.
Poor Performance Even in Single-Threaded CodeEven with no concurrent threads, every Hashtable call acquires and releases a monitor lock. This synchronization overhead makes it measurably slower than HashMap for single-threaded workloads.
Legacy API — Not Recommended for New CodeJava documentation explicitly marks Hashtable as a legacy class. Its design predates the Collections Framework, and it carries baggage like the Dictionary superclass and Enumeration iteration that have no place in modern Java.
No Null Support Creates Rigid ConstraintsWhile null rejection can prevent some bugs, it also means you cannot use null as a sentinel value for 'not found' or 'uninitialized'. Code that naturally uses null keys or values must be redesigned to use Hashtable.
Compound Operations Are Not AtomicIndividual method calls are synchronized but compound operations (containsKey then put, get then put) are not. This creates a false sense of safety — developers assume Hashtable is fully thread-safe and miss race conditions in compound logic.
Unordered and Unpredictable Iteration OrderLike HashMap, Hashtable provides no ordering guarantees. Unlike LinkedHashMap (insertion order) or TreeMap (sorted order), there is no way to get predictable iteration order from a Hashtable.

Java Hashtable — Interview Questions

These are the most frequently asked interview questions on Java Hashtable for Java developer positions. The Hashtable vs HashMap comparison is especially common in technical interviews.

Practice Questions — Test Your Knowledge

Test your understanding of Hashtable with these practice questions. Try answering each one before revealing the answer.

1. What will the following code output? Hashtable<String,Integer> ht = new Hashtable<>(); ht.put("A", 1); ht.put("B", 2); ht.put("A", 3); System.out.println(ht.size() + " " + ht.get("A"));

Easy

2. Which of the following will throw NullPointerException in Hashtable? (a) ht.get(null) (b) ht.put("key", null) (c) ht.put(null, "value") (d) ht.containsValue(null)

Easy

3. Two threads, T1 and T2, both execute: if (!ht.containsKey("seat")) { ht.put("seat", threadName); } on the same Hashtable. Can both threads add an entry for "seat"?

Medium

4. You are maintaining legacy code that uses Hashtable. A new requirement needs null values to represent 'not yet assigned'. What is your approach?

Medium

5. A Hashtable has initialCapacity=11, loadFactor=0.75. After how many put() calls will the first rehash occur?

Medium

6. What is the output? Hashtable<String,String> ht = new Hashtable<>(); ht.put("X", "10"); ht.put("Y", "20"); String val = ht.putIfAbsent("X", "99"); System.out.println(val + " " + ht.get("X"));

Medium

7. Why does Hashtable use a prime number (11) as its default initial capacity while HashMap uses a power of two (16)?

Hard

8. In terms of memory, what does each entry in a Hashtable cost compared to a plain key-value pair?

Hard

Conclusion — Understanding Hashtable's Place in Java

Hashtable occupies a unique place in Java history — it was Java's first key-value data structure, it powered the early years of Java enterprise development, and it introduced the concept of synchronized collections to the JVM ecosystem. Understanding it deeply means understanding why better alternatives like HashMap and ConcurrentHashMap were designed the way they were.

In modern Java development, Hashtable is a museum piece — worth understanding thoroughly for interviews and legacy maintenance, but almost never the right choice for new code. Its coarse-grained locking, null restrictions, and legacy API make it strictly inferior to its successors in every scenario.

ScenarioRecommendation
New single-threaded Map✅ Use HashMap — faster, allows null, modern API
New multi-threaded Map✅ Use ConcurrentHashMap — fine-grained locking, atomic compound ops
Need sorted key order✅ Use TreeMap (single-thread) or ConcurrentSkipListMap (concurrent)
Need insertion-order iteration✅ Use LinkedHashMap
Reading .properties files✅ Use Properties (Hashtable subclass — this is correct and necessary)
Maintaining legacy Hashtable code⚠️ Keep as-is unless refactoring; document the legacy context
Tempted to use Hashtable for thread safety❌ Use ConcurrentHashMap instead — strictly better in every way

Your next steps: study HashMap internals (Java 8's treeification of long chains), ConcurrentHashMap's segment architecture, and the Map interface contract in depth. Together, these form the backbone of key-value storage in virtually every Java application ever written. ☕

Frequently Asked Questions (FAQ)