Key Takeaways
- Use HashMap for non-concurrent use; it allows a null key and null values.
- Hashtable is a legacy synchronized map. Its thread-safe methods don't make a sequence of calls atomic.
- ConcurrentHashMap is usually the right starting point for a shared mutable map, but it rejects null keys and values.
- Collections.synchronizedMap requires callers to lock the returned map while traversing its collection views.
The practical answer to Java HashMap vs Hashtable is straightforward: choose HashMap when you don't need concurrent mutation, and generally consider ConcurrentHashMap when you do. Keep Hashtable for compatibility with existing code rather than making it your default synchronized map.
The Java class is spelled Hashtable, not HashTable. The important differences are synchronization, null handling, iteration, and which operations your application needs to perform atomically.
HashMap vs Hashtable: Comparison Table
| Implementation | Concurrency model | Null support | Iteration order | Typical choice |
|---|---|---|---|---|
| HashMap | No built-in synchronization | One null key; null values | Not guaranteed | Non-concurrent use |
| Hashtable | Individual operations synchronized | No null keys or values | Not guaranteed | Legacy compatibility |
| Collections.synchronizedMap(new HashMap<>()) | Single map-wide lock | One null key; null values | Not guaranteed for a HashMap backing map | Explicitly coordinated access |
| ConcurrentHashMap | Concurrent access; atomic per-key APIs | No null keys or values | Not guaranteed | Shared mutable maps |
Thread Safety: Methods Aren't Transactions
HashMap isn't synchronized. If multiple threads access a mutable HashMap, you need an appropriate synchronization strategy—not just a lock around writes while reads remain unprotected.
Hashtable synchronizes operations such as get and put on the map instance. Under Java's synchronization rules, only one thread can hold that object's monitor, or intrinsic lock, at a time.
That doesn't make check-then-act logic atomic—indivisible relative to competing operations. Two threads can both see containsKey(key) return false before either calls put, allowing one insertion to overwrite the other.
For Hashtable or a synchronized wrapper, protect the entire sequence with synchronized (map). For ConcurrentHashMap, prefer an appropriate atomic operation instead. Our Java multithreading examples provide more background on coordinating shared state.
Safely Iterate Over Collections.synchronizedMap
Collections.synchronizedMap returns a synchronized wrapper around another map. Route all access through that wrapper; retaining and using the original HashMap bypasses its protection.
When traversing entrySet(), keySet(), or values() through an iterator, spliterator, or stream, synchronize on the returned map—not on the collection view. Save this complete example as SynchronizedMapExample.java:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class SynchronizedMapExample {
public static void main(String[] args) {
Map<String, Integer> counts =
Collections.synchronizedMap(new HashMap<>());
counts.put("queued", 2);
counts.put("running", 1);
int total = 0;
synchronized (counts) {
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
total += entry.getValue();
}
}
System.out.println(total); // 3
}
}Keep the locked work short. For expensive processing, consider copying the entries while holding the lock, then processing the copy after releasing it.
Null Keys and Values
HashMap permits one null key and any number of null values. Wrapping it with Collections.synchronizedMap doesn't change that policy. Hashtable and ConcurrentHashMap reject null keys and values; attempting to insert either throws NullPointerException.
With HashMap, get(key) returning null can mean either “missing key” or “key mapped to null.” Use containsKey(key) to distinguish them, with appropriate coordination if the map is shared and mutable.
Before replacing HashMap with ConcurrentHashMap, audit callers that use null as a meaningful value. It's a behavioral change, not merely a different concurrency setting.
Using ConcurrentHashMap
ConcurrentHashMap supports concurrent retrievals and a high expected degree of concurrency for updates. Retrievals such as get generally don't block and can overlap updates. That isn't a promise that every update is lock-free.
Use Atomic Per-Key Operations
ConcurrentHashMap.computeIfAbsent atomically computes and installs a value when the key is absent. This minimal example demonstrates the operation; save it as ConcurrentMapExample.java:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ConcurrentMapExample {
public static void main(String[] args) {
ConcurrentMap<String, Integer> lengths = new ConcurrentHashMap<>();
int length = lengths.computeIfAbsent("Ada", String::length);
System.out.println(length); // 3
}
}If a mapping already exists, the method returns it without invoking the function. A null result installs nothing. Keep the function short, and don't modify the same map from inside it: competing updates may block during computation.
The ConcurrentMap atomic-operation contracts also give you:
putIfAbsent(key, value): insert only if the key has no mapping.replace(key, oldValue, newValue): replace only if the current value matches the expected value.
Separate calls still don't form a transaction. If your invariant spans multiple keys, an atomic operation on one key isn't enough.
Understand Concurrent Iteration
ConcurrentHashMap iterators are weakly consistent: they can traverse during updates without throwing ConcurrentModificationException, but they don't provide a point-in-time snapshot.
Unlike a synchronized wrapper, ConcurrentHashMap doesn't support locking the whole table to prevent all access. Putting your code inside synchronized (map) doesn't make its ordinary operations in other threads acquire that lock. Choose a synchronized wrapper or another explicit coordination design when you need a locked, whole-map workflow.
Map Ordering: LinkedHashMap vs TreeMap
Neither HashMap nor Hashtable guarantees iteration order. An order that looks stable in a test isn't a contract.
- LinkedHashMap normally preserves insertion order; it can also be configured for access order.
- TreeMap orders entries by their keys' natural ordering or a supplied comparator—not insertion order.
Neither alternative is inherently synchronized. Decide ordering and concurrency requirements separately.
Performance Considerations
For non-concurrent use, HashMap avoids synchronization that your application doesn't need. Hashtable and synchronized wrappers can create contention: threads competing for the same lock must wait. Map size alone doesn't determine the performance difference.
Don't assume ConcurrentHashMap wins every benchmark either. Measure representative thread counts, read/write ratios, frequently accessed keys, allocation patterns, and the cost of equals() and hashCode(). Compare only implementations that satisfy the same correctness requirements.
For focused JVM benchmarks, use OpenJDK's Java Microbenchmark Harness (JMH), then validate the result in your application. An unsafe shared HashMap isn't a useful “faster” alternative to a correct concurrent map.
Common Mistakes
- Assuming thread-safe methods protect a workflow. Review every check-then-act and read-modify-write sequence.
- Assuming a thread-safe map makes its values thread-safe. A mutable object retrieved from the map still needs its own concurrency policy; prefer immutable values where practical.
- Using fail-fast exceptions as protection. HashMap and Hashtable iterators detect structural changes only on a best-effort basis. ConcurrentModificationException isn't a synchronization mechanism.
FAQ
What is the difference between HashMap and Hashtable in Java?
HashMap is unsynchronized and permits one null key and null values. Hashtable synchronizes individual operations and rejects null keys and values. Neither guarantees iteration order.
Is Hashtable still recommended for new Java code?
Usually not. Prefer HashMap for non-concurrent use, ConcurrentHashMap for suitable concurrent-map workloads, or a synchronized wrapper when you need explicitly coordinated whole-map access. Keep Hashtable where compatibility requires it.
Can HashMap be safely shared between threads?
Yes, if it's safely published and never modified afterward, or if all access follows an appropriate synchronization strategy. Safe publication means making the initialized state visible to readers, for example by building the map before starting the threads that read it. For concurrent mutation, consider ConcurrentHashMap or a synchronized wrapper.
How does ConcurrentHashMap differ from Collections.synchronizedMap?
A synchronized wrapper serializes access through a shared lock and requires manual locking during collection-view traversal. ConcurrentHashMap supports concurrent access, atomic per-key operations, and weakly consistent iteration, but not a lock that freezes all map operations.
Which Java map implementations allow null keys or values?
Among the implementations compared here, HashMap and a synchronized wrapper around HashMap allow one null key and null values. Hashtable and ConcurrentHashMap allow neither; inserting a null key or value throws NullPointerException.
Further Reading
- HashMap API documentation — null handling, synchronization, and iterator behavior.
- Hashtable API documentation — synchronized behavior and recommended alternatives.
- Collections API documentation — synchronizedMap and its traversal requirements.
- ConcurrentHashMap API documentation — concurrency guarantees and computation methods.
- ConcurrentMap API documentation — atomic conditional updates.
- LinkedHashMap API documentation — insertion and access ordering.
- TreeMap API documentation — sorted-key ordering and comparator requirements.
- JMH documentation and samples — designing JVM benchmarks.
