skip to content

Why does ConcurrentHashMap forbid null keys and null values, when HashMap allows them?

level: middleimportance: should knowfreq 62%

answer

  1. null = ambiguous: absent vs present-with-null
  2. containsKey re-check is racy under concurrency
  3. Both null keys AND null values throw NPE
  4. null is the sentinel for putIfAbsent/compute/merge
  5. Use Optional/sentinel instead of null

basics

~20 s

Because of an ambiguity: if get(key) returned null, you couldn't tell whether the key is absent or present with a null value. In a single thread you could re-check with containsKey, but under concurrency another thread could change the answer between calls, so nulls are banned outright.

solid answer

~40 s

ConcurrentHashMap throws NullPointerException on a null key or value. The reason is that null is overloaded to mean 'no mapping': get returning null could mean the key is absent or that it maps to null. In a single-threaded HashMap you disambiguate with containsKey, but in a concurrent map that two-step check is racy—another thread can insert or remove the key between get and containsKey, so the result is meaningless. Doug Lea's rationale is that null carries no useful, well-defined meaning in a concurrent map and almost always signals a programming bug, so it is rejected up front rather than allowed to create subtle races. This also keeps the atomic methods (putIfAbsent, computeIfAbsent, merge) unambiguous, since they use null to signal 'absent' or 'remove the mapping'.

go deeper

for a junior

Knows ConcurrentHashMap throws NullPointerException for null keys or values, while HashMap permits them.

for a middle

Explains the absent-vs-present-with-null ambiguity and why the containsKey re-check that saves HashMap is racy under concurrency.

for a senior

Connects the ban to the sentinel role null plays in putIfAbsent/compute/merge and cites Doug Lea's 'no sound concurrent meaning' rationale.

for a principal

Discusses API-contract design implications: how disallowing nulls simplifies the atomic-method contracts and prevents whole classes of latent concurrency bugs, and recommends Optional/sentinel patterns at scale.

## The core ambiguity With any map, `get(key)` returning `null` is **ambiguous** in general: it can mean either (a) **the key is not in the map**, or (b) **the key is in the map and its value is `null`**. To tell these apart you'd call `containsKey(key)`, which returns a boolean. ## Why HashMap can live with it but ConcurrentHashMap cannot In a **single-threaded** `HashMap`, the two-step disambiguation works: ``` if (map.containsKey(k)) { // step 1 Object v = map.get(k); // step 2 } ``` Between step 1 and step 2 nothing can change the map, because only one thread is involved. In a **concurrent** map, no such guarantee exists. Between `get` (which returned `null`) and a follow-up `containsKey`, **another thread can insert or remove the key**. So the answer you compute is already stale—there is **no correct way** for a caller to resolve the ambiguity. Doug Lea (the author) decided that since `null` cannot be given a sound, race-free meaning here, and a null value almost always indicates a bug, the map **rejects nulls outright** with a `NullPointerException` (for both keys and values). ## Keys vs values - **Null values** are forbidden for the ambiguity reason above. - **Null keys** are also forbidden. There's no strong technical *need* the way there is for values, but it keeps the contract uniform and avoids special-casing a single sentinel key in the concurrent algorithms. ## It also keeps the atomic methods clean The compound atomic methods rely on `null` as a **sentinel**: - `putIfAbsent(k, v)` returns the **existing** value, or `null` if there was none (and then it inserts `v`). - `computeIfAbsent` / `compute` / `merge` use a remapping function whose returning `null` means **"remove the mapping"**. If stored values could themselves be `null`, these sentinels would collide with real data and the methods couldn't signal "absent" vs "present-but-null" or "remove" unambiguously. Banning null values makes every one of these signals precise. ## Practical guidance - Never pass `null` as a key or value to a CHM—you'll get an NPE, often surprisingly (e.g. a value that is computed and happens to be null). - If you need to represent "known to be absent" vs "present with no value", use an explicit sentinel object or wrap in `Optional` and store the Optional, never a raw null.

  • HashMap allows nulls; how many null keys can it hold?
    Exactly one null key (plus any number of null values). ConcurrentHashMap allows neither.
  • If a computeIfAbsent mapping function returns null, what happens?
    No mapping is recorded and computeIfAbsent returns null. In compute/merge, returning null removes any existing mapping. That's why stored null values would clash with this sentinel.

saying these in an interview costs you the question

  • Saying only null values are forbidden, not null keys
  • Claiming HashMap also forbids nulls (it allows one null key and null values)
  • Thinking you can safely disambiguate with containsKey in a concurrent map
  • Believing it's an arbitrary restriction with no rationale

context