HashMap允许null键是因为其hash()方法对null返回0,使null键固定存入table[0],并通过==判断而非equals()保证唯一性;而HashTable和ConcurrentHashMap因线程安全设计显式禁止null键。
因为 hash() 方法对 null 做了特殊处理:当 key == null 时,直接返回 0,不调用 key.hashCode() —— 避开了空指针异常。这个哈希值 0 会定位到桶数组(Node[] table)的索引 0 位置。
null 键最终都落在数组下标 0 处,后续插入时通过 == null 判断是否为同一个 key(不是用 equals())== null 比较,且哈希值固定,所以天然保证“最多一个 null 键”map.get(null) 时,内部也是先算 hash(null) → 0,再遍历 table[0] 链表或红黑树,找那个 key == null 的节点它们在 put() 入口就做显式判空,且没绕过 hashCode() 调用:
HashTable 源码里有 if (value == null) throw new NullPointerException(),且 int hash = key.hashCode() 直接执行 —— key 为 null 必崩ConcurrentHashMap 同样禁止 null 键/值,核心原因是并发歧义:比如 get(key) == null 时,无法区分“key 不存在”还是“key 存在但 value 是 null”,而 containsKey() 又不能完全替代(存在竞态窗口)允许 ≠ 推荐。null 键容易引发逻辑混淆,尤其在读取时:
map.get(null) == null 判断 null 键是否存在 —— 它可能是没这个键,也可能是键存在但值是 null
map.containsKey(null) 确认键存在,再用 map.get(null) 取值private static final Object NO_KEY = new Object();),避免 null 带来的二义性null 键忽略或报错,生产环境若涉及跨语言传输,要提前验证static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
就这一行三目运算,是整个 null 键可行性的技术支点。它不依赖 Object.hashCode() 的契约,而是由 HashMap 自己接管语义 —— 这也是为什么只有 HashMap(及 LinkedHashMap、WeakHashMap 等继承者)能这么干,而其他 Map 实现必须各自实现自己的 null 处理逻辑或直接拒绝。
真正容易被忽略的,不是“能不能放”,而是“取出来之后怎么解释”。null 键本身很轻量,但它的语义重量,往往在三个月后的某次线上排查里才突然显现。