HashMap vs Hashtable
HashMap and Hashtable both implement the Map interface and store key-value pairs using a hash table internally — but they differ in ways that matter a lot in real applications. Understanding these differences will help you write correct, performant code and avoid subtle concurrency bugs.
Quick Comparison Table
| Feature | HashMap | Hashtable |
|---|---|---|
| Introduced | Java 1.2 | Java 1.0 (legacy) |
| Thread-safe | No | Yes (synchronized) |
| Null keys | 1 allowed | Not allowed |
| Null values | Multiple allowed | Not allowed |
| Iteration order | Not guaranteed | Not guaranteed |
| Iterator type | Fail-fast | Fail-safe (Enumerator) |
| Performance | Faster (no sync overhead) | Slower |
| Extends | AbstractMap | Dictionary (legacy) |
| Recommended today | Yes | Rarely — prefer ConcurrentHashMap |
The Null Key / Null Value Difference
This is the most common source of runtime surprises. HashMap accepts one null key and any number of null values. Hashtable throws a NullPointerException the moment you try either.
import java.util.HashMap;
import java.util.Hashtable;
public class NullDemo {
public static void main(String[] args) {
// HashMap: null key and null value are fine
HashMap<String, String> map = new HashMap<>();
map.put(null, "noKey");
map.put("name", null);
System.out.println(map.get(null)); // noKey
System.out.println(map.get("name")); // null
// Hashtable: throws NullPointerException
Hashtable<String, String> table = new Hashtable<>();
try {
table.put(null, "value"); // NPE here
} catch (NullPointerException e) {
System.out.println("Hashtable rejected null key: " + e);
}
}
}
Output:
noKey
null
Hashtable rejected null key: java.lang.NullPointerException
Thread Safety
Hashtable synchronizes every method — put(), get(), remove(), and more. That means only one thread can operate on it at a time. It’s safe, but it’s slow when there is contention.
HashMap has no built-in synchronization. In a single-threaded or carefully controlled context that is perfectly fine and noticeably faster. In a concurrent context you need an explicit strategy.
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeOptions {
public static void main(String[] args) {
// Option 1: wrap HashMap (coarse-grained lock, similar to Hashtable)
Map<String, Integer> syncMap =
Collections.synchronizedMap(new HashMap<>());
// Option 2: ConcurrentHashMap (fine-grained segment locks, far better)
Map<String, Integer> concMap = new ConcurrentHashMap<>();
concMap.put("score", 42);
System.out.println(concMap.get("score")); // 42
}
}
Output:
42
Tip: In virtually every concurrent scenario today you should reach for
ConcurrentHashMaprather thanHashtable. It uses bucket-level locking (and, since Java 8, CAS operations) so multiple threads can read and write different buckets simultaneously. See Concurrent Collections for the full picture.
Iteration: Fail-Fast vs Legacy Enumerator
HashMap’s iterators are fail-fast: if you structurally modify the map while iterating (outside of the iterator’s own remove()), a ConcurrentModificationException is thrown immediately. This helps you detect bugs early.
Hashtable predates the Iterator interface. It exposes an older Enumeration which does not throw ConcurrentModificationException — it may silently return stale data.
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class FailFastDemo {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 80);
Iterator<String> it = scores.keySet().iterator();
try {
while (it.hasNext()) {
String key = it.next();
scores.put("Charlie", 70); // structural modification mid-iteration
}
} catch (java.util.ConcurrentModificationException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
}
}
Output:
Caught: ConcurrentModificationException
Warning: Fail-fast behavior is a best-effort diagnostic tool — it is not guaranteed to fire on every unsynchronized modification. Never rely on it for correctness; use it only as a development-time safety net.
Performance in Practice
Because Hashtable acquires a lock on every operation, even reads, it becomes a bottleneck under multi-threaded load. HashMap with no synchronization is fastest for single-threaded code. ConcurrentHashMap wins for concurrent workloads because it only locks at the bucket level.
import java.util.HashMap;
import java.util.Hashtable;
public class SimplePerf {
static final int OPS = 1_000_000;
public static void main(String[] args) {
HashMap<Integer, Integer> hm = new HashMap<>();
Hashtable<Integer, Integer> ht = new Hashtable<>();
long start = System.nanoTime();
for (int i = 0; i < OPS; i++) hm.put(i, i);
long hmTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < OPS; i++) ht.put(i, i);
long htTime = System.nanoTime() - start;
System.out.printf("HashMap: %d ms%n", hmTime / 1_000_000);
System.out.printf("Hashtable: %d ms%n", htTime / 1_000_000);
}
}
Note: Actual timings vary by JVM warmup and hardware. The consistent pattern is that
HashMapoutperformsHashtablebecause it avoids synchronization overhead on every call.
Under the Hood
Both HashMap and Hashtable maintain an internal array of buckets (the table). Each bucket holds a linked list (or, since Java 8, a balanced red-black tree for buckets with more than 8 entries) of Entry / Node objects.
When you call put(key, value):
key.hashCode()is called to get a raw hash.- The hash is spread with a supplemental mixing function to reduce clustering.
hash & (capacity - 1)maps the spread hash to a bucket index.- If a collision exists, the chain/tree is walked using
equals().
Where they differ internally:
Hashtable.put()is declaredsynchronized, so the JVM acquires the intrinsic lock on theHashtableobject before any of the above steps. This prevents any other thread from calling any synchronized method concurrently — a very coarse lock.HashMap.put()has no such declaration. The JVM compiles it with no lock acquisition, making the method faster but not safe for concurrent use without external coordination.Hashtablealso disallows null keys by explicitly checkingif (value == null) throw new NullPointerException()at the start ofput(). The null-key check is implicit:key.hashCode()would throw an NPE naturally, so no special guard is needed.
The default initial capacity of HashMap is 16 with a load factor of 0.75. Hashtable’s default initial capacity is 11 — a prime number, which was a common hash-table design choice in earlier Java to reduce collisions with the modulo-based index calculation it originally used (before the bitwise AND trick that requires power-of-two sizes).
When to Use What
- Single-threaded code — use
HashMap. It is faster, more flexible (allowsnull), and is part of the modern Collections Framework. - Multi-threaded code — use
ConcurrentHashMap. It provides thread safety with far better throughput thanHashtable. - Synchronized wrapper —
Collections.synchronizedMap(new HashMap<>())is acceptable for simple cases but still uses a single coarse lock, similar toHashtable. - Legacy code / maintenance — you will encounter
Hashtablein older codebases. Understand it, but avoid introducing new uses.
Tip: If you need an insertion-ordered map, consider LinkedHashMap. If you need sorted keys, look at TreeMap.
Related Topics
- HashMap — deep dive into HashMap internals, hashing, and resizing
- Working of HashMap — how hashing, buckets, and tree-bins work under the hood
- Concurrent Collections — ConcurrentHashMap and other thread-safe alternatives
- LinkedHashMap — insertion-ordered map built on HashMap
- TreeMap — sorted key map using a red-black tree
- Synchronization — understanding Java’s intrinsic locks and the synchronized keyword