HashMap源码阅读——红黑树

本文解析了Java 1.8中HashMap如何利用红黑树提高性能。关键参数包括树化阈值、链表还原阈值及最小树形化容量。通过treeifyBin方法实现链表到红黑树的转换。

HashMap源码阅读——红黑树

上节我们提到了jdk1.8中引入了红黑树来解决一个桶下链表过长的问题。

关键参数

HashMap中有三个关于红黑树的关键参数

//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;
//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

红黑树定义

  • 红黑树的每个节点的颜色非红即黑
  • 根节点是黑色的
  • 空叶子节点是黑色的
  • 如果一个节点是红色的,那么它的子节点必须是黑色的
  • 从任意节点都该节点的子孙节点的所有路径上包含相同数目的黑点树
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)

treeifyBin

HashMap中树形化最重要的一个方法treeifyBin() 即树形化。在一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是8),就使用红黑树来替换链表。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果hash表为空或者hash表的容量小于MIN_TREEIFY_CAPACITY(64),那么就去新建或者扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                //新建一个树形节点,内容和当前链表节点一致
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;   //头节点
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //之前得到的只是一个链表状的二叉树,下一步格式化红黑树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

红黑树的具体格式化过程

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
   for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {   //根节点
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {  //遍历已经生成的红黑树
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)//这里的hash是经过扰动函数之后得到的hash
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&     //hash一样且(key值不可比较或者两者key值相等)
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root); //把根节点放到桶中
}

可以看到,将二叉树变为红黑树时,需要保证有序。这里有个双重循环,拿树中的所有节点和当前节点的哈希值进行对比(如果哈希值相等,就对比键,这里不用完全有序,只是保证rebalance时的插入一致性)

参考文献

Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构
重温数据结构:深入理解红黑树
红黑树代码实现

### ✅ 回答:为什么 `HashMap` 要用红黑树? 在 Java 8 及以后版本中,当 `HashMap` 中某个桶(bucket)的链表长度 **超过一定阈值(默认是 8)**,并且当前数组长度 ≥ 64 时,该链表会 **自动转换为红黑树**。 这么做是为了: > 🔥 **防止哈希冲突严重时,查找性能从 O(1) 恶到 O(n),降级为链表遍历** 通过升级为红黑树,将最坏情况下的查找时间复杂度优为 **O(log n)**,从而保障高负载下的性能稳定性。 --- ## 🧠 一、HashMap 的底层结构演变 Java 8 之前: ``` 数组 + 链表 ``` Java 8 及之后: ``` 数组 + 链表 + 红黑树(当链表过长时) ``` ### 结构示意图(简): ``` Index 0: null Index 1: [Node] → [Node] → [Node] → ... (普通链表) Index 2: null ... Index k: 红黑树(TreeNode) ← 当链表长度 ≥ 8 且 table.length ≥ 64 ``` --- ## 🔍 二、为什么要引入红黑树?根本原因:**避免哈希碰撞攻击(Hash Collision Attack)** ### 场景分析: 假设所有 key 的 `hashCode()` 都一样(或映射到同一个 bucket),那么它们都会被放在同一个链表中。 比如: ```java map.put(new Key("a"), "value1"); map.put(new Key("b"), "value2"); // 所有 Key 的 hashCode() 返回相同值 ``` 结果就是: - 所有元素都挤在一个桶里 - 插入、查找、删除操作退成 **遍历链表** - 时间复杂度从理想情况 O(1) → 最坏情况 O(n) 如果 n 很大(比如上万个 key 冲突),性能急剧下降! 💡 攻击者甚至可以通过构造恶意输入造成 **DoS(拒绝服务)攻击** —— 这就是所谓的“哈希碰撞 Dos”。 --- ## ✅ 解决方案:链表转红黑树 | 条件 | 行为 | |------|------| | 单个桶中链表节点数 ≥ 8 | 并且哈希表长度 ≥ 64 | | → 触发(treeify) | 链表 → 红黑树 | 这样即使发生严重哈希冲突,操作的时间复杂度也最多是 **O(log n)**,而不是 O(n)。 > ⚖️ 权衡说明:虽然红黑树插入稍慢(常数更大),但对数级增长比线性好得多。 --- ## 📈 性能对比:链表 vs 红黑树 | 操作 | 链表(O(n)) | 红黑树(O(log n)) | |------|--------------|--------------------| | 查找 | 10,000 次比较 | ~14 次比较(log₂10000 ≈ 13.3) | | 插入 | O(n) | O(log n) | | 删除 | O(n) | O(log n) | ✅ 明显优势:数据越多,差距越大! --- ## 🔧 三、源码层面的关键点(Java 8+) ### 相关参数定义(在 HashMap 源码中): ```java static final int TREEIFY_THRESHOLD = 8; // 链表转阈值 static final int UNTREEIFY_THRESHOLD = 6; // 转回链表阈值 static final int MIN_TREEIFY_CAPACITY = 64; // 最小哈希表容量才允许 ``` ### 的条件总结: 只有同时满足以下两个条件才会: 1. 当前桶中的节点数量 ≥ 8 2. 哈希表的容量(数组长度)≥ 64 否则,会选择先进行 **扩容(resize)** 来缓解冲突。 --- ## 🌲 四、红黑树的特点为何适合这里? | 特性 | 说明 | |------|------| | 自平衡二叉搜索 | 插入/查找/删除都能保持 O(log n) | | 左旋右旋调整 | 维持近似平衡,避免退成链表 | | 存储开销略高 | 每个节点多几个字段(父指针、颜色标志等) | | 但在极端情况下更安全 | 抵御恶意哈希碰撞 | Java 中的 `TreeNode` 是 `LinkedHashMap.Entry` 的子类,内部实现了红黑树所需的所有逻辑。 --- ## ❓ 常见疑问解答 ### Q1:为什么不一开始就用红黑树? ❌ 不划算!因为: - 大多数情况下哈希分布均匀,链表很短(平均 < 1) - 红黑树节点占用内存更多(多了 parent、left、right、color 等) - 插入删除维护成本更高(旋转、染色) 👉 所以只在必要时才“升维打击”——即 **惰性(lazy treeify)** --- ### Q2:什么时候会从红黑树变回链表? 当调用 `resize()` 扩容后,数据重新分布,某些桶中的节点数减少。 如果节点数 ≤ 6(`UNTREEIFY_THRESHOLD`),就会 **反(untreeify)** 成链表。 --- ## ✅ 总结:HashMap 为什么要用红黑树? | 原因 | 说明 | |------|------| | ✅ 提升最坏情况性能 | 将 O(n) → O(log n) | | ✅ 防止哈希碰撞攻击 | 安全性增强 | | ✅ 动态适应负载变 | 只在需要时才 | | ✅ 权衡空间与时间 | 正常情况用链表,极端情况用 | 这是 Java 设计者在 **性能、安全、内存、通用性** 上做出的优秀权衡。 --- ###
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值