# 问题

13. 为什么 ConcurrentHashMap 不支持 key 或 value 为 null?

# 标准答案

ConcurrentHashMap 不支持 null 作为 key 或 value,主要原因是:

  1. 避免歧义:如果 map.get(key) 返回 null,无法判断 key 不存在 还是 key 存在但对应的 value 为 null
  2. 提升并发安全性:在高并发环境下,null 可能引发 非预期异常并发竞争问题,容易导致 NullPointerException 或并发 bug。
  3. 简化内部实现:JDK 采用 computeIfAbsent() 等方法处理 key-value 关系,null 可能导致逻辑复杂化,影响代码健壮性。

相比之下,HashMap 允许 null key 和 null value,因为它主要用于单线程或低并发场景,不需要考虑这些复杂情况。

# 答案解析

# 1. 避免歧义

HashMap 中,如果 map.get(key) 返回 null,有两种可能:

  1. key 根本不存在(即 containsKey(key) == false)。
  2. key 存在但对应的 value 为 null(即 containsKey(key) == true)。

在单线程环境中,我们可以用 containsKey() 进一步判断 key 是否存在。但在高并发环境下,这种判断方式可能导致 竞态条件(race condition),导致 containsKey() 结果过时。例如:

if (!map.containsKey(key)) {
    // 在多线程环境下,这里 key 可能已经被其他线程 put 进去了
    return map.get(key); 
}
1
2
3
4

如果 null 是合法值,get() 可能返回 null,而此时 key 可能已经存在,导致业务逻辑混乱。

因此,ConcurrentHashMap 禁止 null,确保 map.get(key) == null 一定是 key 不存在,避免语义歧义。

# 2. 避免并发异常

ConcurrentHashMapcomputeIfAbsent()putIfAbsent() 等方法中,null 值可能导致 意外的并发异常,例如:

map.computeIfAbsent(key, k -> null);
1

如果 null 允许存入,某些操作可能导致 NullPointerException,影响线程安全。例如:

map.put(key, null);
map.get(key).someMethod(); // NullPointerException
1
2

如果 ConcurrentHashMap 允许 null,多个线程可能同时读取 null 值,导致 非预期异常,严重影响系统稳定性。

# 3. 简化内部实现

ConcurrentHashMap 采用 分段锁(JDK 1.7)或 CAS + 链表/红黑树(JDK 1.8) 来保证并发安全。允许 null 值会使代码更复杂,例如:

  1. 存储结构额外处理 null:需要额外的标记字段区分 key-value 为空的情况。
  2. CAS 操作复杂化:CAS 不能直接操作 null,需要额外的包装。
  3. API 设计一致性:如 computeIfAbsent() 期望 lambda 返回非 null,允许 null 会导致 API 语义不清晰。

因此,JDK 直接禁止 null,避免不必要的复杂性。

# 深入追问

🔹 为什么 Hashtable 允许 null value,而 ConcurrentHashMap 不允许?
🔹 如果业务确实需要 null value,应该如何设计?
🔹 ConcurrentHashMap 禁止 null 是否影响其兼容性?

# 相关面试题

ConcurrentHashMap 和 HashMap 在存储 key-value 方面有哪些差异?
如何在 ConcurrentHashMap 中优雅地存储 null value?
ConcurrentHashMap 如何保证高并发情况下的安全性?