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.
// 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 addedHow put(key, value) works step by step:
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.
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.
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.
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 → 78Load 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.
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.
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.
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.
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.
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.
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 HeadphonesBest 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)
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: 2Example 2 — Migrating from Hashtable to ConcurrentHashMap
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: 49Practice 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.
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"));
Easy2. 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)
Easy3. 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"?
Medium4. You are maintaining legacy code that uses Hashtable. A new requirement needs null values to represent 'not yet assigned'. What is your approach?
Medium5. A Hashtable has initialCapacity=11, loadFactor=0.75. After how many put() calls will the first rehash occur?
Medium6. 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"));
Medium7. Why does Hashtable use a prime number (11) as its default initial capacity while HashMap uses a power of two (16)?
Hard8. In terms of memory, what does each entry in a Hashtable cost compared to a plain key-value pair?
HardConclusion — 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.
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. ☕