HashMap允许null键和null值,Hashtable不允许任何null;HashMap默认容量16(2的幂,位运算寻址),Hashtable默认11(奇数,取模寻址);HashMap有红黑树优化,Hashtable无;Hashtable迭代器不fail-fast,HashMap迭代器会抛ConcurrentModificationException。

HashMap 和 Hashtable 谁能存 null?一写就报错的坑
这是开发中最常踩的雷:Hashtable 对 null 零容忍,只要 put(null, "x") 或 put("k", null),立刻抛 NullPointerException;而 HashMap 允许一个 null 键(存在数组索引 0 的位置)和任意多个 null 值。
- 如果你业务里需要把“未设置”“未知来源”等语义用
null表达为 key,只能选HashMap - 用
Hashtable时别依赖 IDE 自动补全——它不会提前告诉你put方法内部有if (key == null) throw new NullPointerException() - 注意:即使你用
Collections.synchronizedMap(new HashMap()),null支持仍保留,但线程安全是“假安全”(全局锁,性能差)
多线程环境下直接用 Hashtable 真的安全吗?
语法上安全,逻辑上危险。它的所有方法都加了 synchronized,但锁的是整个对象——相当于排队打饭,一个人打完,下一个人才能进窗口。并发高时,get 和 put 互相阻塞,吞吐量断崖下跌。
-
ConcurrentHashMap是现代替代方案:JDK 1.8+ 用 CAS + synchronized 分段锁(实际是 Node 数组分桶加锁),读操作无锁,写操作只锁冲突桶 - 千万别为了“图省事”在 Spring Bean 里注入
Hashtable当共享缓存——哪怕 QPS 只有 50,响应延迟也可能翻倍 - 如果必须兼容老代码且不能改类型,至少确认它没被高频
put/remove,否则扩容时的锁竞争会更严重
为什么 HashMap 默认容量是 16,而 Hashtable 是 11?
这不是随意定的数字,背后是哈希寻址效率差异:
-
HashMap容量必须是 2 的幂(16、32、64…),这样算桶索引用位运算hash & (capacity - 1),比取模快一个数量级 -
Hashtable容量默认 11(奇数),扩容公式是newCapacity = oldCapacity * 2 + 1,设计初衷是让hash % capacity分布更均匀——但现代 CPU 上,取模成本远高于位运算,这个优化已失效 - 实测:插入 10 万条数据,
HashMap平均耗时约 8ms,Hashtable约 14ms(JDK 17,无并发)
HashMap 的红黑树优化,Hashtable 为什么没有?
因为 Hashtable 是 JDK 1.0 的遗留类,从诞生就没考虑过海量哈希冲突场景;而 HashMap 在 JDK 1.8 引入红黑树,专门解决“链表过长 → 查询退化成 O(n)”的问题。
立即学习“Java免费学习笔记(深入)”;
- 触发条件很明确:
链表长度 ≥ 8且数组容量 ≥ 64,才转红黑树;反之,节点 ≤ 6 就降级回链表 - 这意味着:如果业务中 key 的
hashCode()实现不合理(比如总返回同一个值),Hashtable的查询会随数据增长线性变慢,而HashMap仍能保持 O(log n) - 别指望靠调大
initialCapacity规避——真正要治本,得重写 key 类的hashCode()和equals()
真正容易被忽略的点是:Hashtable 的 Enumeration 迭代器不支持 fail-fast,遍历时被其他线程修改也不会报错,结果可能漏数据或重复读;而 HashMap 的 Iterator 一旦检测到并发修改,立刻抛 ConcurrentModificationException——这看似是“缺点”,其实是帮你提前暴露线程安全问题。










