🔑 Section 10 · Question #25

Can HashMap Store a null Value

Unlike keys (which must remain unique, restricting HashMap to at most one null key), any number of distinct keys can map to a null value.


🟢 Junior Level

Yes, HashMap allows storing null as a value, with no limits on the count.

Unlike keys (which must remain unique, restricting HashMap to at most one null key), any number of distinct keys can map to a null value.

import java.util.HashMap;
import java.util.Map;

public class NullValueDemo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        map.put("key1", null);
        map.put("key2", null);
        map.put("key3", "Active");

        System.out.println(map.size());       // 3 (all three entries are persisted)
        System.out.println(map.get("key1"));  // null
    }
}

The Fundamental Ambiguity Dilemma

When map.get(key) returns null, two mutually exclusive realities are possible:

  1. The requested key does not exist in the map.
  2. The key exists, but is explicitly associated with a null value.

To reliably differentiate between these two scenarios, you must query map.containsKey(key):

if (map.get(key) == null) {
    if (map.containsKey(key)) {
        System.out.println("The key exists, but its value is null.");
    } else {
        System.out.println("The key does not exist in the map.");
    }
}

Analogy: Imagine post office mailboxes. In mailbox #1, there is a physical, empty blank envelope with no letter inside (a null value). Mailbox #2 was never installed on the wall at all. Looking into either mailbox gives you no readable message, but mailbox #1 physically exists in the directory.


🟡 Middle Level

Java 8+ Methods Semantics with null Values

Methods introduced in the Java 8 Map interface have critical, non-obvious rules when interacting with null values:

1. getOrDefault(key, defaultValue)

If the key is present in the map but associated with null, the method returns null, NOT the default value:

Map<String, String> map = new HashMap<>();
map.put("user", null);

String val = map.getOrDefault("user", "Anonymous");
System.out.println(val); // Prints null, NOT "Anonymous"!

Rule: getOrDefault falls back to defaultValue only if the key is completely absent (!map.containsKey(key)).

2. putIfAbsent(key, value)

If the key is present in the map but its current value is null, putIfAbsent overwrites null with the new value:

Map<String, String> map = new HashMap<>();
map.put("config", null);

map.putIfAbsent("config", "default_timeout");
System.out.println(map.get("config")); // "default_timeout"

JDK Specification: “If the specified key is not already associated with a value or is mapped to null, associates it with the given value.”

3. computeIfAbsent(key, mappingFunction)

Identical to putIfAbsent: if a key is mapped to null, the mapping function executes and stores the computed result:

map.put("data", null);
map.computeIfAbsent("data", k -> calculateExpensivePayload()); // calculateExpensivePayload() executes!

4. merge(key, value, remappingFunction)

If the existing value is null, merge simply saves the new value without invoking the remapping function. If the remapping function returns null, the entry is completely removed from the map:

map.put("counter", 10);
map.merge("counter", 5, (oldV, newV) -> null); // Removes "counter" from the map entirely!
System.out.println(map.containsKey("counter")); // false

Map Support Matrix for null Values

Map Implementation null Values Allowed Reaction to map.put(key, null)
HashMap ✅ Yes (Unlimited) Successfully stores Node with value = null
LinkedHashMap ✅ Yes (Unlimited) Stores Node with value = null, maintains iteration order
TreeMap ✅ Yes (Unlimited) Allowed without restriction (only keys require sorting)
ConcurrentHashMap ❌ Forbidden Throws NullPointerException immediately
Hashtable ❌ Forbidden Throws NullPointerException immediately
Map.of(...) (Java 9+) ❌ Forbidden Throws NullPointerException immediately

🔴 Senior Level

Physical Heap Representation in OpenJDK HotSpot

Inside the internal node structure of HashMap:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;            // Reference to the value object
    Node<K,V> next;
}
  • The value field is a standard 4-byte reference pointer (with -XX:+UseCompressedOops enabled).
  • Storing null populates the value field with machine zeros (0x00000000).
  • Memory Fallacy: Storing null does not save node allocation overhead. The full Node<K,V> object is still allocated on the heap, consuming 32 bytes (plus its slot reference in table[]).

Why ConcurrentHashMap and Hashtable Forbid null Values

In concurrent programming, separating “key not found” from “value is null” using get() and containsKey() is fundamentally impossible due to a TOCTOU (Time of Check to Time of Use) race condition:

// Hypothetical unsafe concurrent code:
if (map.get(key) == null) {
    // Another thread removes or mutates the entry right at this instant!
    if (map.containsKey(key)) {
        // Thread assumes key is mapped to null, but it has already been removed!
    }
}

By forbidding null values, ConcurrentHashMap enforces a rock-solid invariant: map.get(key) == null atomically and unambiguously proves that the key is absent.

Architectural Anti-Patterns vs Clean Patterns

The Anti-Pattern: Map<String, Optional<User>>

Some developers attempt to resolve the ambiguity by wrapping values in Optional:

// ❌ ANTI-PATTERN:
Map<String, Optional<User>> cache = new HashMap<>();
cache.put("alice", Optional.empty());

Why this is detrimental:

  1. Optional was designed strictly as a return type for methods to indicate absence, not as a persistent field or collection element.
  2. Optional does not implement java.io.Serializable. Placing Optional inside collections breaks serialization across network boundaries (e.g., Hazelcast, Redis serializers, RMI).
  3. Every Optional.empty() or Optional.of() instance allocates an additional 16–24 bytes on the heap, creating unnecessary memory overhead and GC pressure.

Clean Architectural Patterns:

  1. Optional.ofNullable() at Retrieval Boundaries:
    Optional<User> user = Optional.ofNullable(map.get(userId));
    
  2. The Null Object Pattern: Associate the key with a typed singleton sentinel object (e.g., User.ANONYMOUS) exhibiting no-op behavior.
  3. Explicit Removal: If data no longer exists or becomes invalidated, remove the entry via map.remove(key) instead of setting its value to null.

4 Tricky Questions

1. What does map.getOrDefault("key", "default") return if "key" is mapped to null?

Answer: It returns null, NOT "default". The OpenJDK implementation of Map.getOrDefault is:

default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

If the key exists in the map (containsKey(key) == true), the ternary operator selects v (which is null), completely ignoring defaultValue. The default value is returned only when the key is physically absent.


2. How does putIfAbsent("k", "v") behave if the map contains ("k", null)?

Answer: It replaces null with "v" and returns null (the previous value). Under the Map specification, putIfAbsent inserts the new value not only when the key is absent, but also whenever the existing value associated with the key is null:

default V putIfAbsent(K key, V value) {
    V v = get(key);
    if (v == null) {
        v = put(key, value);
    }
    return v;
}

3. Does TreeMap allow storing null as a value?

Answer: Yes, unconditionally and without limits. TreeMap enforces strict ordering constraints exclusively on its keys (keys must implement Comparable or match a supplied Comparator). Values are stored as passive payloads inside tree nodes (Entry.value) and are never compared during Red-Black Tree traversals, insertions, or balancing.


4. What happens if the remapping function in map.merge(key, value, remappingFunction) returns null?

Answer: The key and its corresponding entry are completely deleted from the HashMap. Inside the implementation of merge:

V oldValue = p.val;
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
if (newValue != null) {
    p.val = newValue;
} else {
    // If remapping returns null, the node is purged!
    removeNode(hash, key, null, false, true);
}

This serves as an idiomatic and atomic mechanism for conditional removal in Java 8+.


🎯 Interview Cheat Sheet

  • Permission: HashMap allows an unlimited number of null values.
  • The Ambiguity Trap: map.get(key) == null cannot distinguish a missing key from a key mapped to null. Always verify with map.containsKey(key).
  • getOrDefault Trap: If a key exists with value null, getOrDefault returns null, NOT the fallback value.
  • putIfAbsent Behavior: Overwrites the existing value if it is currently null.
  • merge Behavior: If the remapping function returns null, the entry is physically removed from the map.
  • Where null Values Are Forbidden: ConcurrentHashMap, Hashtable, and Map.of(...) throw NullPointerException.
  • Anti-Pattern: Never use Map<K, Optional<V>> (breaks serialization, incurs memory/GC penalty). Instead, use Optional.ofNullable(map.get(k)) at query time.