为什么 JDK 1.8 对 HashMap 进行了红黑树的改动?
·
JDK 1.8 对 HashMap 引入红黑树优化是为了解决在特定场景下的性能问题,主要原因如下:
1. 解决哈希冲突导致的性能退化
在 JDK 1.7 及之前,HashMap 在哈希冲突时使用链表解决:
// JDK 1.7: 链表结构
Entry<K,V>[] table;
Entry<K,V> {
K key;
V value;
Entry<K,V> next; // 链表指针
}
问题: 当大量元素哈希到同一个桶时,链表会变得很长,查询时间复杂度从 O(1) 退化为 O(n)
2. 极端场景下的性能灾难
// 恶意构造的哈希冲突攻击
Map<String, String> map = new HashMap<>();
// 所有元素的 hash 值都相同
for (int i = 0; i < 100000; i++) {
map.put("AaAa" + i, "value" + i); // hash 值相同
}
// 结果: 所有元素都在同一个链表中,查询性能急剧下降
影响:
- 查询时间: O(1) → O(n)
- 可能导致 DoS 攻击
- 服务器响应时间显著增加
3. 红黑树的优势
JDK 1.8 引入红黑树后:
// JDK 1.8: 链表 + 红黑树混合结构
Node<K,V>[] table;
Node<K,V> { // 链表节点
K key;
V value;
Node<K,V> next;
}
TreeNode<K,V> extends Node<K,V> { // 红黑树节点
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
boolean red;
}
性能对比:
链表查询: O(n)
红黑树查询: O(log n)
当 n = 1000:
链表: 平均 500 次比较
红黑树: 约 10 次比较 (log₂1000 ≈ 10)
4. 树化阈值的设计
// 树化条件
static final int TREEIFY_THRESHOLD = 8; // 链表长度 ≥ 8 时树化
static final int UNTREEIFY_THRESHOLD = 6; // 树节点 ≤ 6 时退化为链表
// 树化逻辑
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
为什么是 8?
- 根据泊松分布计算,负载因子 0.75 时,链表长度达到 8 的概率极低 (约 0.00000006)
- 正常情况下很少触发树化
- 只在哈希冲突严重时才树化,避免不必要的开销
5. 性能提升的量化分析
// 测试场景: 10000 个元素,严重哈希冲突
// JDK 1.7 (纯链表)
查询时间: ~500ms
// JDK 1.8 (红黑树)
查询时间: ~5ms
性能提升: 100 倍
6. 内存和时间的权衡
红黑树的额外开销:
- 每个节点需要额外的指针 (parent, left, right)
- 内存占用增加约 2 倍
- 树化/退树化操作有一定开销
权衡策略:
- 只在必要时树化 (链表长度 ≥ 8)
- 元素减少时及时退树化 (节点数 ≤ 6)
- 避免频繁的树结构转换
7. 代码实现对比
// JDK 1.7: 链表查询
public V get(Object key) {
Entry<K,V> e = getEntry(key);
if (e == null) return null;
return e.value;
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) { // 遍历链表
if (e.hash == hash && key.equals(e.key))
return e;
}
return null;
}
// JDK 1.8: 智能选择链表或红黑树
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// ...
if (first instanceof TreeNode) // 红黑树查询
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 链表查询
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
return null;
}
8. 安全性提升
红黑树优化有效防止了:
- 哈希碰撞 DoS 攻击: 恶意构造相同 hash 值的键
- 性能拒绝服务: 通过大量冲突导致服务响应缓慢
- 资源耗尽: 长链表导致的 CPU 和内存消耗
9. 实际应用场景
// 场景 1: 缓存系统
Map<String, Object> cache = new HashMap<>();
// 大量缓存键可能产生冲突,红黑树保证查询性能
// 场景 2: 统计计数
Map<String, Integer> counter = new HashMap<>();
// 统计词频时,某些词可能大量重复
// 场景 3: 配置管理
Map<String, String> config = new HashMap<>();
// 配置键可能设计不当导致冲突
10. 与其他数据结构的对比
数据结构 查询 插入 删除 空间
─────────────────────────────────────────
数组 O(1) O(1) O(n) 小
链表 O(n) O(1) O(1) 小
二叉搜索树 O(log n) O(log n) O(log n) 中
红黑树 O(log n) O(log n) O(log n) 中
HashMap(链表) O(1) O(1) O(1) 小
HashMap(红黑树) O(log n) O(log n) O(log n) 中
总结
JDK 1.8 引入红黑树的核心原因:
- ✅ 解决性能退化: 防止链表过长导致的 O(n) 查询
- ✅ 提升安全性: 防止哈希碰撞 DoS 攻击
- ✅ 智能优化: 根据实际情况自动选择最优数据结构
- ✅ 工程平衡: 在性能、内存、复杂度之间取得最佳平衡
- ✅ 向后兼容: 不影响正常使用场景,只在极端情况下生效
这个改动体现了 Java 集合框架在性能优化和安全防护方面的持续改进,是 HashMap 发展史上的重要里程碑。
更多推荐

所有评论(0)