What is the difference between HashMap, Hashtable and ConcurrentHashMap?

All three store key-value pairs using hashing, but they differ in thread safety, performance and null handling. HashMap is not thread-safe and is the default choice. Hashtable is an old, fully synchronized class. ConcurrentHashMap is the modern thread-safe map, designed for high concurrency.

Key Points: • HashMap: not synchronized, fastest in single-threaded code, allows one null key and many null values. • Hashtable: every method is synchronized on the whole table, so only one thread can use it at a time. It allows no null keys or values, and it is a legacy class from Java 1.0. • ConcurrentHashMap: thread-safe without locking the whole map. Since Java 8, it uses CAS operations and locks only individual buckets on writes, and reads don't lock at all. • ConcurrentHashMap doesn't allow nulls, because null would be ambiguous ("no mapping" or "mapped to null") in concurrent code. • Iterators: HashMap and Hashtable iterators are fail-fast. ConcurrentHashMap's iterators are weakly consistent and never throw ConcurrentModificationException. • Use Collections.synchronizedMap() only when you need a synchronized wrapper around another Map type.

Code Example:

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MapComparison {

    static void tryNull(String name, Map<String, String> map) {
        try {
            map.put(null, "value");
            System.out.println(name + ": null key allowed");
        } catch (NullPointerException e) {
            System.out.println(name + ": null key NOT allowed");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        tryNull("HashMap", new HashMap<>());
        tryNull("Hashtable", new Hashtable<>());
        tryNull("ConcurrentHashMap", new ConcurrentHashMap<>());

        // 4 threads x 10,000 increments on the same key
        Map<String, Integer> counts = new ConcurrentHashMap<>();
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int t = 0; t < 4; t++) {
            pool.submit(() -> {
                for (int i = 0; i < 10_000; i++) counts.merge("hits", 1, Integer::sum);  // atomic
            });
        }
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("ConcurrentHashMap count: " + counts.get("hits"));
    }
}

Output:

HashMap: null key allowed Hashtable: null key NOT allowed ConcurrentHashMap: null key NOT allowed ConcurrentHashMap count: 40000

Good to Know: Thread-safe methods don't make compound actions thread-safe. "if (!map.containsKey(k)) map.put(k, v)" can still race on a ConcurrentHashMap. Use the atomic methods putIfAbsent(), computeIfAbsent() and merge() instead, as in the example.

Interview Tip: A concise interview answer is:

"HashMap isn't thread-safe but is the fastest and allows one null key. Hashtable is a legacy class that synchronizes every method on the whole table and allows no nulls. ConcurrentHashMap is the modern thread-safe choice: it locks only individual buckets on writes, reads without locking, and doesn't allow nulls. For compound updates I use its atomic methods like merge() and computeIfAbsent()."

Previous question: What are the initial capacity and load factor of a HashMap, and what happens when it resizes?

Next question: What is the difference between List.of(), Collections.unmodifiableList() and Arrays.asList()?

More Java Collections interview questions

All Java Collections questions