ConcurrentHashMap 明确禁止 null 键和值,因并发下 null 会导致语义二义性;所有写入方法均在入口校验并抛 NPE;应使用 Optional、哨兵对象或拆分 Map 等方式安全替代。

ConcurrentHashMap.put(null, "x") 直接抛 NullPointerException
不是运行时偶然崩溃,而是源码里硬编码的防御逻辑——putVal 方法第一行就检查:if (key == null || value == null) throw new NullPointerException();。你根本走不到哈希计算或桶操作那步。
- 所有写入入口(
put、putIfAbsent、compute等)最终都落到这个校验上 -
remove(key)和replace(key, oldValue, newValue)也要求key非空,否则同样 NPE - 别指望绕过:哪怕用反射或 Unsafe 强行塞进去,后续
get或扩容时大概率触发不可预测行为
为什么 HashMap 能存 null,ConcurrentHashMap 就不行?二义性是真问题
假设线程 A 执行 concurrentHashMap.get("k") 返回 null,你无法区分这是“key 不存在”,还是“key 存在但 value 显式设为 null”——而这两个语义在并发场景下完全不可互换。
- HashMap 单线程下可用
containsKey()辅助判断,但ConcurrentHashMap的containsKey()和get()不是原子组合,中间可能被其他线程修改 - 像
computeIfAbsent(key, f)这类方法依赖“查无此 key 才计算”,如果null是合法 value,整个语义就垮了 - 更隐蔽的是迭代器:
ConcurrentHashMap的弱一致性迭代器不保证看到所有更新,若允许 null value,遍历时遇到null就彻底失去上下文
实际开发中怎么安全替代 null 场景?
不能存 null 不等于不能表达“缺失/未初始化/无效”状态——关键在于用明确语义的非 null 值代替。
- value 为 null 的常见意图(如缓存未命中、配置未设置):改用
Optional<V>包装,或定义哨兵对象(如MISSING枚举) - key 为 null 的需求(比如默认配置兜底):改用特殊字符串键,如
"__DEFAULT__",比运行时 NPE 更早暴露设计问题 - 需要“存在性 + 值”双重语义时,考虑拆成两个
ConcurrentHashMap:一个存 key → value,另一个用ConcurrentHashMap<K, Boolean>单独标记是否“已显式设置”
容易忽略的隐式 null 来源
很多 NPE 并非手写 put(null, ...),而是上游逻辑无意透出 null。
- JSON 反序列化字段缺失时,Jackson 默认设为 null;Gson 同理——务必配
@JsonInclude(JsonInclude.Include.NON_NULL)或自定义反序列化器 - 数据库查询结果映射:MyBatis 的
<if test="xxx != null">漏写,或 JPA 实体字段没加@Column(nullable = false),都可能导致 null 流入 Map - 函数式接口返回值:比如
map.computeIfPresent(k, (k, v) -> mayReturnNull()),若mayReturnNull()返回 null,computeIfPresent会直接删除该 key,而非设为 null —— 这个行为本身也说明 null 不被当作有效值










