【Java集合】HashMap的扩容操作源码详解

22 篇文章 4 订阅
14 篇文章 31 订阅

目录

1 split()

2 untreeify()

3 treeify()

3.1 comparableClassFor()

 3.2 compareComparables()

4 tieBreakOrder()

5 balanceInsertion()

 5.1 rotateLeft()

 5.2 rotateRight()

 6 moveRootToFront()

 7 checkInvariants()


HashMap中的resize()扩容方法会触发一系列的TreeNode类的方法,依次为:split()、untreeify()、treeify()、tieBreakOrder()、balanceInsertion()、moveRootToFront()、checkInvariants()

下面我们将按照扩容流程依次详细讲解方法源码。

调用开始位置:

final Node<K,V>[] resize() {
    ......
    // 判断当前的e是不是红黑树
    else if (e instanceof TreeNode)
        // 拆分树,重新更新HashMap扩容后红黑树中元素的位置
        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    ......
}

1 split()

拆分操作,随着元素的插入,hashMap还会扩容,红黑树和链表因为扩容的原因导致原本在一个数组元素下的Node节点分为高低位两部分,低位树即当前原位置,高位树则在新扩容的tab上(原位置index + 扩容大小)。这一块难点就是 低位红黑树和高位红黑树的处理,至于(e.hash & bit) == 0 这个问题在之前的HashMap源码讲解的文章中已经解释过了,这里的原理和对链表进行高低位处理时的相同。

/**
* 将红黑中的节点拆分为较高位红黑树和低位红黑树,或者如果树现在太小,则取消树化
*
* @param index 当前红黑树所在数组位置
* @param bit 当前数组容量,也就是二倍扩容后要增加的容量大小
* @param map 这里将HashMap<K,V> map传入进来是为了调用HashMap类中的replacementNode()方法,用来实现TreeNode转为Node的操作。
* @param tab HashMap中的数组
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    // 获得调用此方法的节点b,这个节点是存储在数组桶中的TreeNode节点,也就是红黑树的根节点
    TreeNode<K,V> b = this;
    // 重新链接到 lo 和 hi 列表,保留顺序
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null; // 低位树存储索引位置为:“原索引位置”的节点  loHead为链表头节点,loTail为链表尾节点
    TreeNode<K,V> hiHead = null, hiTail = null; // 高位树存储索引位置为:“原索引+oldCap”的节点   hiHead为链表头节点,hiTail为链表尾节点
    // lc 低位红黑树的节点数,hc 高位红黑树的节点数
    int lc = 0, hc = 0;
    // 从节点b开始,遍历整个红黑树节点。这里遍历红黑树是采用链表遍历的方法,因为TreeNode在维护节点的红黑树结构的同时,也维护了链表结构,所以既可以通过红黑树结构遍历,也可以通过链表结构遍历
    // 这个循环操作是将所有的节点遍历一篇,判断出哪些节点需要留在原位置,哪些节点需要升到高位,所以这里只需要通过链表方式把所有节点都遍历一遍就可以了。
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        // next赋值为e的下个节点
        next = (TreeNode<K,V>)e.next; 
        // 得到e的next后,将e的next指向null 以便垃圾收集器回收
        e.next = null;
        // 注意此处&运算的数组容量没有-1
        // 那么数组的容量值二进制表达必定为:1000...,所以此处计算只有两个结果,1或者0
        // 0:TreeNode在新数组的位置是原位置,将节点放入低位树;1:原位置加上旧数组容量值的位置,将节点放入高位树。
        if ((e.hash & bit) == 0) {
            // 将loTail节点变成e节点的前节点,
            // 若loTail节点不存在,代表该节点为第一个节点
            if ((e.prev = loTail) == null)
                //将e节点赋值给loHead节点,loHead指向第一个节点
                loHead = e;
            else
                //存在则将e节点赋值给loTail的后节点
                loTail.next = e;
            //将e节点赋值给loTail节点
            loTail = e;
            //计算低位红黑树的节点数
            ++lc;
        }
        // 以下操作和上方操作一样
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    // 如果低位红黑树存在
    if (loHead != null) {
        // 如果低位红黑树节点数量小于等于红黑树瓦解阈值6,
        if (lc <= UNTREEIFY_THRESHOLD)
            // 将低位红黑树转为链表,并将头节点放到原位置的桶中
            tab[index] = loHead.untreeify(map);
        else {
            // 否则将低位红黑树根节点放到数组上
            tab[index] = loHead;
            // 高位树不存在则说明原先的节点都在当前红黑树上。则就不需要变化了
            if (hiHead != null) // (else is already treeified) 
                // 如果高位红黑树存在,则将低位红黑树重新树化,
                // 虽然当前已经是红黑树了,但是节点改变了,所以要重新进行一遍树化,梳理节点
                loHead.treeify(tab);
        }
    }
    
    //以下操作与上方操作一样
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            // 将高位红黑树转为链表,并将根节点放到新位置的桶中
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                // 如果低位红黑树存在,则将高位红黑树重新树化,
                hiHead.treeify(tab);
        }
    }
}

 以上对红黑树的处理 涉及到 树化 和 反树化 ,也就是链表和红黑树的互相转换,下面我们来看一下反树化和树化代码。

2 untreeify()

将红黑树节点转为链表节点(反树化), 当节点<=6个时会被触发。

/**
 * 将红黑树节点转为链表节点, 当节点<=6个时会被触发
 * @param map 这里将HashMap<K,V> map传入进来是为了调用HashMap类中的replacementNode()方法,来构建Node节点进而生成链表。
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
    // hd指向头节点, tl指向尾节点
    Node<K,V> hd = null, tl = null; 
    // 从调用该方法的节点, 即链表(当前这个链表的节点都是TreeNode红黑树节点)的头节点开始遍历, 将所有TreeNode节点全转为Node链表节点
    for (Node<K,V> q = this; q != null; q = q.next) {
        // 调用HashMap的replacementNode()方法,将树节点构建成链表节点
        Node<K,V> p = map.replacementNode(q, null);
        // 如果tl为null, 则代表当前节点为第一个节点, 将hd指向p
        if (tl == null)
            hd = p;
        // 否则, 将尾节点的next指向当前节点p,也就是进行链表追加
        else
            tl.next = p;
        
        // 每次循环q都会后移一个,同理p也就是后移之后构建出来的链表节点
        tl = p; // 将tl节点指向链表节点p, 即尾节点
    }
    // 返回转换后的链表的头节点
    return hd;
}
// 构建链表节点
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
    return new Node<>(p.hash, p.key, p.value, next);
}

treeify()

以当前调用该方法的TreeNode为初始节点,遍历处理链表(使用TreeNode的链表结构)上的每个节点来进行树化,把遍历到的链表上的节点重新插入到新构建的红黑树结构中,每次插入树节点都要进行平衡处理,保证红黑树的平衡。这样就完成了重新的树化操作。根据这个方法的执行过程也可以看成是将链表结构转换为红黑树结构的方法。

/**
 * 红黑树树化操作
 * @param tab 在后续需要调用moveRootToFront方法将构建好的红黑树节点的根节点放置在tab数组的桶中
 */
final void treeify(Node<K,V>[] tab) {
    // 定义红黑树的根节点
    TreeNode<K,V> root = null; 
    // 遍历链表(使用TreeNode红黑树的链表结构来进行遍历),x指向当前节点、next指向下一个节点。初始化将x指向调用treeify()方法的TreeNode节点
    // 整个树化流程就是将需要重新树化的红黑树结构以它的链表形态进行遍历,将遍历到的每一个节点插入到新构建好的红黑树结构中,这里就会进行一个遍历红黑树的操作,每次插入一个节点就重新对新构建好的红黑树进行平衡处理。
    // 下面我们将遍历链表结构的当前节点x叫做链表节点,将后面遍历红黑树结构时的当前节点p成为红黑树节点,但需要注意它俩其实都是TreeNode节点,x节点所在的链表也只是使用了红黑树结构的链表形式,其本身也是一个红黑树
    for (TreeNode<K,V> x = this, next; x != null; x = next) { 
        // 下一个链表节点
        next = (TreeNode<K,V>)x.next; 
        // 设置当前链表节点的左右子节点为空,清空以前的红黑树结构,准备构建新的红黑树结构
        x.left = x.right = null; 
        // 如果新构建的红黑树还没有根节点,则将调用treeify()的节点设置为根节点
        if (root == null) {
            // 当前节点的父节点设为空
            x.parent = null; 
            // 当前节点的红色属性设为false(把当前节点设为黑色)
            x.red = false;
            // 根节点指向到当前节点(当前节点设置为根节点),将该链表节点插入到新构建红黑树的根节点位置
            root = x; 
        }
        // 如果已经存在根节点了,说明非第一次操作,则将x节点添加到root节点的子树上
        else {
            // 取得当前遍历到链表节点的key 
            K k = x.key; 
            // 取得当前遍历到链表节点的hash值
            int h = x.hash; 
            // 定义key所属的Class
            Class<?> kc = null; 
            // 从已经构建好的红黑树的根节点开始遍历,这里采用的是红黑树结构的遍历,此遍历没有设置边界,只能从内部跳出
            // p表示当前遍历到的节点,我们在这里可以将其称作红黑树节点,外面一层循环的x节点我们可以称其为链表节点,方便后续操作的区分
            for (TreeNode<K,V> p = root;;) { 
                // GOTO1
                // dir 表示方向(左右)、ph表示当前红黑树节点的hash值
                int dir, ph; 
                // pk表示当前树节点的key
                K pk = p.key; 
                // 如果当前树节点hash值 大于 当前链表节点的hash值
                if ((ph = p.hash) > h) 
                    // 表示当前链表节点会放到当前树节点的左侧
                    dir = -1; 
                else if (ph < h)
                    // 右侧
                    dir = 1; 
 
                /*
                * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                * 下面这个if分支就是先判断kc是否等于null,注意它的后面是且运算符,所以当判断kc等于null的时候还要继续向下判断,kc==null说明kc还没有被赋值。
                * 然后再去调用comparableClassFor方法来判断当前节点的key是否实现了comparable接口,如果返回的是null说明没有实现,如果实现了,就会返回Key的class类型,这里对kc进行了赋值。
                * kc==null&&(kc = comparableClassFor(k)) == null是一个与运算组合,然后它门后面就是一个或运算,也就是如果kc没有被赋值并且调用comparableClassFor返回是null,也就是这个与运算表达式结果是true,那么整个或运算结果就是true,也不会继续执行后面的代码了,直接进入到if分支内部,因为如果是null说明它没有实现comparable接口,也就不能使用compareComparables方法来判断大小了,需要进入到if分支使用tieBreakOrder()来判断大小。
                * 但是如果返回不是null,说明key实现了comparable接口,可以使用compareComparables方法来比较大小,则会继续执行后面的代码。
                * 当确定了key实现了comparalble接口以后,就可以执行后面调用compareComparables方法的代码,来进行大小的判断,并且将判断大小的结果赋值给dir,只要dir!=0,说明compareComparables成功判断出了两个数据的大小关系,就不用进入if分支内使用tieBreakOrder来进行大小的比较了,
                * 但是如果返回的结果是0,说明compareComparables也没能比较出两者大小(比较的两个对象使用compareComparables方法比较也相等或者两者Class类型不一致或者要比较的有一方为null这个方法就会返回0),还是需要tieBreakOrder来比较。
                */
                else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
 
                // 保存当前树节点
                TreeNode<K,V> xp = p; 
 
                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
                 * 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点  再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
                 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。每一个链表节点最后都会被插入到红黑树的叶子节点下
                 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
                 */
                // if判断力的语句是先根据dir的值来将p.left或p.right赋值给p,然后再判断p是不是null
                // 如果p是null,说明它是叶子节点,直接将x链表节点插入到p的左子节点或右子节点即可
                // 如果不是null,说明不是叶子节点,则以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;
                }
            }
        }
    }
 
    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 因为我们要基于树来做查找,确保给定的根节点是其 tab 中的节点(存放在数组桶的节点)。
    moveRootToFront(tab, root); 
}

这个方法中在判断链表节点和树节点时,如果链表节点的hash值小于树节点的hash值的时候,链表节点就会作为树节点的左节点;hash值大于树结点的hash值的时候就作为树节点的右节点。如果发现它俩的key和hash值相等,这时还是会先尝试看是否能够通过Comparable进行比较一下两个对象,要想看看是否能基于Comparable进行比较的话,首先要看该元素键是否实现了Comparable接口,此时就需要用到comparableClassFor方法来获取该元素键的Class,然后再通过compareComparables方法来比较两个对象的大小。这里就是用了HashMap的两个方法comparableClassFor()和compareComparables(),TreeNode是HashMap的内部类,可以直接调用HashMap的方法。

如果两者不具有compare的资格,或者compare之后仍然没有比较出大小。那么最后就要通过tieBreakOrder方法再比较一次。

3.1 comparableClassFor()

/**
 * Returns x's Class if it is of the form "class C implements
 * Comparable<C>", else null.
 * 如果对象x的类是C,如果C实现了Comparable<C>接口,那么返回C,否则返回null。这个方法的作用就是查看对象x是否实现了Comparable接口
 */
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        // 如果x是个字符串对象,直接返回String.class
        if ((c = x.getClass()) == String.class) 
            // 返回String.class
            return c; 
        /*
         * 为什么如果x是个字符串就直接返回c了呢 ? 因为String  实现了 Comparable 接口,可参考如下String类的定义
         * public final class String implements java.io.Serializable, Comparable<String>, CharSequence
         */ 
 
        // 如果 c 不是字符串类,获取c直接实现的接口(如果是泛型接口则附带泛型信息)    
        if ((ts = c.getGenericInterfaces()) != null) {
            // 遍历接口数组
            for (int i = 0; i < ts.length; ++i) { 
                // 如果当前接口t是个泛型接口 
                // 如果该泛型接口t的原始类型p 是 Comparable 接口
                // 如果该Comparable接口p只定义了一个泛型参数
                // 如果这一个泛型参数的类型就是c,那么返回c
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                        Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
            // 上面for循环的目的就是为了看看x的class是否 implements  Comparable<x的class>
        }
    }
    // 如果c并没有实现 Comparable<c> 那么返回空
    return null; 
}

 3.2 compareComparables()

/**
  * Returns k.compareTo(x) if x matches kc (k's screened comparable
  * class), else 0.
  * 如果x的类型是kc,返回k.compareTo(x)的比较结果
  * 如果x为空,或者类型不是kc,返回0
  * @param kc 对象k的Class类型
  * @param k  标准对象k
  * @param x  比较对象x
  */
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

4 tieBreakOrder()

比较两个对象的大小,返回值只能大于0或小于0,不能为0,因为需要插入节点是放在左子树还是右子树,这里在两个对象都不为空时,先使用compareTo的方法比较两个对象的类名按字符串规则比较,如果类名比较不出来或者为空则调用native方法System.identityHashCode()去比较key的hashcode值,相等时返回-1,否则返回1

/**
 * 比较a和b的大小,-1:a<=b;1:a>b
 */
static int tieBreakOrder(Object a, Object b) {
    int d;
    //对象a和b都不为null则进行比较
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        //如果通过compareTo方法不能解决,则通过native的System.identityHashCode方法
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}

5 balanceInsertion()

这里先简单说一下红黑树的平衡调整。红黑树是一种自平衡二叉树,拥有优秀的查询和插入/删除性能,广泛应用于关联数组。

对比 AVL 树,AVL 要求每个节点的左右子树的高度之差的绝对值(平衡因子)最多为 1,而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡调整耗时,从而获取更好的性能,而这虽然会导致红黑树的查询会比 AVL 稍慢,但相比插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。

在 HashMap 中的应用:HashMap 在进行插入和删除时有可能会触发红黑树的插入平衡调整(balanceInsertion 方法)删除平衡调整(balanceDeletion 方法),调整的方式主要有以下手段:左旋转(rotateLeft 方法)、右旋转(rotateRight 方法)、改变节点颜色(x.red = false、x.red = true),进行调整的原因是为了维持红黑树的数据结构。

红黑树的插入平衡,通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求。在插入节点之后进行平衡调整,x为新添加的节点,root为树的根节点,返回根节点。

注释中写的各种章节,是这篇文章中对应的情况【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树icon-default.png?t=M4ADhttps://blog.csdn.net/cy973071263/article/details/122543826?spm=1001.2014.3001.5501

/**
 * 调用该方法,是因为在红黑树插入新节点之后,可能会出现红黑树的失衡,需要重新进行平衡
 * @param root 当前根节点
 * @param x 新插入的节点
 * 
 * @return 返回重新平衡后的根节点
 */
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    // 新插入的节点标为红色                                                    
    x.red = true; 
 
    /*
     * 这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
     * xp:当前节点的父节点、xpp:祖父节点、xppl:左叔父节点、xppr:右叔父节点
     */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { 
 
        // 如果父节点为空、说明当前节点就是根节点,那么把当前节点标为黑色,返回当前节点
        if ((xp = x.parent) == null) { 
            x.red = false;
            // 当前节点x即为红黑树的根节点
            return x;
        }
 
        // 父节点不为空
        // 如果父节点为黑色,则无需处理即可维持红黑树性质,直接返回根节点。
        // 后面这一段(xpp = xp.parent) == null根本就不可能出现这种情况,这里这样写只是为了对xpp祖父节点进行的复制,进而继续后面的操作。并不会真的出现(xpp = xp.parent) == null的情况导致直接返回根节点
        else if (!xp.red || (xpp = xp.parent) == null) // 6.2.1.1节
            return root;
        // 如果执行到这里,说明新插入节点的父节点是红色。之前讲过,父节点为红色一共有8种情况,全部都需要进行处理才能维持红黑树性质
        // 如果父节点是祖父节点的左孩子 
        if (xp == (xppl = xpp.left)) { 
            // 如果右叔父不为空 并且为红色,即上溢LL插入情况和上溢LR情况
            if ((xppr = xpp.right) != null && xppr.red) { // 上溢LL是6.2.4节,上溢LR是6.2.6节,这两种的修复方法一样
                // 右叔父置为黑色
                xppr.red = false; 
                // 父节点置为黑色
                xp.red = false; 
                // 祖父节点置为红色
                xpp.red = true; 
                // 运行到这里之后,就又会进行下一轮的循环了,将祖父节点当做处理的起始节点(当成新的插入节点处理)
                x = xpp; 
            }
            else { // 如果右叔父为空或者为黑色,即右叔父节点不为红色
                // 如果插入节点是父节点的右孩子,即LR插入情况,该情况的修复需要插入节点的父节点左旋,祖父节点右旋  
                if (x == xp.right) { // 6.2.3节
                    // 父节点左旋,并且将当前节点设置为原父节点,见下文左旋方法解析
                    root = rotateLeft(root, x = xp);
                    // 获取祖父节点(相对于新的x节点的祖父节点)。这个是为了后面需要将祖父节点进行右旋转,所以要获取一下祖父节点。
                    // 但是这里可能会比较绕,那就是这个时候x已经表示的不是原来的插入节点了,而是变成了插入节点的原父节点,这里实际上找的是旋转之后的插入节点的原父节点现在的祖父节点(这里的xp = x.parent指向的实际是插入节点,此时插入节点已经变成了其原父节点的父节点,两者颠倒了一下位置)
                    // 但旋转过后现在原父节点已经下降了一层了,所以向上走两代其实找到的还是原来的那个相对于插入节点的祖父节点,旋转并没有影响到祖父节点那一层的变化,所以这里是没有问题的。结合自旋图例更容易理解。
                    xpp = (xp = x.parent) == null ? null : xp.parent; 
                }
                // 如果插入节点是父节点的左节点,上面的if分支是不会执行的,只会执行下面的if分支。如果只执行下面的分支,说明是LL插入情况,修复方法是将祖父节点右旋。 6.2.2节
                // 如果执行了上面的if分支,还要执行下面的if分支,说明是LR插入情况
                // 如果父节点不为空,将祖父节点右旋来完成修复
                if (xp != null) { 
                    // 父节点置为黑色。此时的xp就是最开始的插入节点,只是因为自旋的原因换了位置。最开始插入节点x的父节点进行左旋,调用左旋方法的时候将插入节点x赋值给了原父节点,在经过左旋之后,原父节点下降了一层,原插入节点上升了一层,原插入节点成了原父节点的父节点,所以更新过的xp的父节点其实就是最开始的插入节点原x节点,想要将插入节点变成黑色,就需要把xp设置为黑色
                    xp.red = false; 
                    // 祖父节点不为空
                    if (xpp != null) { 
                        // 祖父节点置为红色
                        xpp.red = true;
                        // 祖父节点右旋,见下文右旋方法解析
                        root = rotateRight(root, xpp);  
                    }
                }
            }
        }
        else { // 如果父节点是祖父节点的右孩子
            // 如果左叔父节点是红色,即为上溢的RR插入情况和上溢的RL插入情况
            if (xppl != null && xppl.red) { // 上溢RR是6.2.5节,上溢RL是6.2.7节,这两种的修复方法一样
                // 左叔父节点置为 黑色
                xppl.red = false; 
                // 父节点置为黑色
                xp.red = false; 
                // 祖父节点置为红色
                xpp.red = true; 
                // 运行到这里之后,就又会进行下一轮的循环了,将祖父节点当做处理的起始节点 
                x = xpp; 
            }
            else { // 如果左叔叔为空或者是黑色,即左叔父节点不为红色
                // 如果插入节点是父节点的左孩子,即RL插入情况,该情况的修复需要插入节点的父节点右旋,祖父节点左旋  
                if (x == xp.left) { // 6.2.3节
                    // 针对父节点做右旋,见下文右旋方法解析
                    root = rotateRight(root, x = xp); 
                    // 获取祖父节点,这个是为了后面需要将祖父节点进行左旋转,所以要获取一下祖父节点。
                    xpp = (xp = x.parent) == null ? null : xp.parent; 
                }
                // 如果插入节点是父节点的右节点,上面的if分支是不会执行的,只会执行下面的if分支。如果只执行下面的分支,说明是RR插入情况,修复方法是将祖父节点左旋。 6.2.2节
                // 如果执行了上面的if分支,还要执行下面的if分支,说明是RL插入情况
                // 如果父节点不为空 
                if (xp != null) { 
                    // 父节点置为黑色。此时的xp就是最开始的插入节点,只是因为自旋的原因换了位置。
                    xp.red = false; 
                    // 如果祖父节点不为空
                    if (xpp != null) { 
                        // 爷爷节点置为红色
                        xpp.red = true; 
                        // 针对祖父节点做左旋
                        root = rotateLeft(root, xpp); 
                    }
                }
            }
        }
    }
}

 5.1 rotateLeft()

// p:图示中的 E, r:图示中的 S,  rl:图示最开始S节点的左子节点,只有这个节点做了位置的较大改动,从S的子节点变成了E的子节点
// E节点由上层转移到了下层,S节点由下层转移到了上层
/**
 * 节点左旋
 * root 根节点
 * p 要左旋的节点
 */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
    // r:要左旋节点的右子节点  pp:要左旋节点的父节点  rl:要左旋节点的右孩子的左子节点
    TreeNode<K,V> r, pp, rl;
    // 要左旋的节点 以及 要左旋的节点的右孩子 不为空。此时将要左旋节点的右孩子赋值给r
    if (p != null && (r = p.right) != null) { 
        // 下面要重新设置要左旋节点的右子节点的左子节点的位置,因为旋转过程中只有这个节点有较大的位置变动,以图为例它的父节点由S变成了E
        // 要左旋的节点的右孩子的左节点 赋给 要左旋的节点的右孩子。此时将要左旋节点的右孩子的左子节点赋值给rl
        if ((rl = p.right = r.left) != null) 
            // 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
            rl.parent = p; 
        // 下面就是要重新设置左旋节点父节点的新子节点,因为左旋节点已经被旋转到了下一层,左旋节点原来的位置被旋转上来一个新的元素,需要重新设置父子关系
 
        // 将要左旋的节点的右孩子的父节点 指向 要左旋的节点的父节点,相当于右孩子提升了一层。此时将要左旋节点的父节点赋值给pp
        // 此时如果父节点为空,说明r已经是顶层节点了,应该作为root并且标为黑色
        if ((pp = r.parent = p.parent) == null) 
            (root = r).red = false;
        // 如果父节点不为空 并且 要左旋的节点是个左孩子
        else if (pp.left == p) 
            // 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
            pp.left = r; 
        else // 要左旋的节点是个右孩子
            pp.right = r; 
        // 重新设置左旋节点和左旋节点的右子节点的父子关系,因为此时左旋节点被旋转到了下一层,左旋节点的右子节点旋转到了上一层,两者的父子关系进行了对调
        // 要左旋的节点 作为 它的右孩子的左节点  【爹认孩子】
        r.left = p; 
        // 要左旋的节点的右孩子 作为 它的父节点  【孩子认爹】
        p.parent = r; 
    }
    // 返回根节点
    return root; 
}

 5.2 rotateRight()

// p:图示中的 S,l:图示中的 E,lr:图示最开始E节点的右子节点,只有这个节点做了位置的较大改动,从E的子节点变成了S的子节点
// E节点由下层转移到了上层,S节点由上层转移到了下层
/**
 * 节点右旋
 * root 根节点
 * p 要右旋的节点
 */
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
    // l:要右旋节点的左子节点  pp:要右旋节点的父节点  lr:要右旋的节点的左孩子的右节点
    TreeNode<K,V> l, pp, lr;
    // 要右旋的节点不为空 以及 要右旋的节点的左孩子不为空。此时将要右旋节点的左子节点赋值给l
    if (p != null && (l = p.left) != null) {
        // 下面要重新设置要右旋节点的左子节点的右子节点的位置,因为旋转过程中只有这个节点有较大的位置变动,以图为例它的父节点由E变成了S
        // 要右旋的节点的左孩子的右节点 赋给 要右旋节点的左孩子。此时将要右旋的节点的左孩子的右节点赋值给lr 
        if ((lr = p.left = l.right) != null) 
            // 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
            lr.parent = p; 
        // 下面就是要重新设置右旋节点父节点的新子节点,因为右旋节点已经被旋转到了下一层,右旋节点原来的位置被旋转上来一个新的元素,需要重新设置父子关系
 
        // 将要右旋的节点的左孩子的父节点 指向 要右旋的节点的父节点,相当于左孩子提升了一层。此时将要右旋节点的父节点赋值给pp
        // 此时如果父节点为空, 说明l已经是顶层节点了,应该作为root 并且标为黑色
        if ((pp = l.parent = p.parent) == null) 
            (root = l).red = false;
        // 如果父节点不为空 并且 要右旋的节点是个右孩子
        else if (pp.right == p) 
            // 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
            pp.right = l; 
        else // 要右旋的节点是个左孩子
            // 同上
            pp.left = l; 
        
        // 下面要重新设置右旋节点和右旋节点的左子节点的父子关系,因为此时右旋节点被旋转到了下一层,右旋节点的左子节点旋转到了上一层,两者的父子关系进行了对调
        
        // 要右旋的节点 作为 他左孩子的右节点   【爹认孩子】
        l.right = p; 
        // 要右旋的节点的父节点 指向 他的左孩子 【孩子认爹】
        p.parent = l; 
    }
    // 返回根节点
    return root;
}

 6 moveRootToFront()

当我们删除或者增加红黑树节点的时候,root节点在双链表中的位置可能会变动,为了保证每次红黑树的根节点都在链表的第一个位置(也就是红黑树根节点需要在数组桶中),在操作完成之后需要moveRootToFront方法来进行调整,将root节点移动到数组桶中。

TreeNode<K,V> extends LinkedHashMap.Entry<K,V>因为TreeNode继承了Entry。所以它除了它自己几个属性 parent 、left、 right、 prev、 red、之外,还有继承过来的属性hash、key、value 、next。因为有prev和next属性,所以它底层其实是双结构的,维护着红黑树和双向链表两种结构。所以当每次调整好红黑树之后,root节点的位置可能会变动,红黑树的位置关系改变了,但是双向链表的关系没有变,那这个时候我们就要重新维护root在双向链表中的关系了,这里注意下,重新维护时不是修改红黑树结构而是修改的链表结构的prev和next属性,进而将table的桶指向新的root节点(移动双向链表中的元素并不会影响红黑树,同样移动红黑树也不会影响双向链表)。

/**
 * 将root放到头节点(链表结构的头节点,也就是将root节点放到数组桶中)的位置, 原头节点(原来在数组桶中的节点)放在root的next节点上,即保证树的根节点一定也要成为链表的头节点
 * 把红黑树的根节点放置在数组桶中
 */
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    // 当前数组tab的长度
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        // 找到当前树所在的bin桶位置(即数组tab的位置)
        int index = (n - 1) & root.hash;
        // 将tab[index]的树节点记录下来为first
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        // 判断当前root和该root应该在的数组桶位置上的元素是不是同一个,如果不是,说明root没有落在tab数组上,则将root调整放置在tab数组上
        if (root != first) {
            Node<K,V> rn;
            // 这里即替换掉tab[index]指向的原有节点,可以理解成现在指向root节点
            tab[index] = root;
            // rp为root指向的前一个节点
            TreeNode<K,V> rp = root.prev;
            // rn为root的后一个节点
            // 将root前后节点关联
            if ((rn = root.next) != null)
                // root节点的next节点的prev前驱节点指向root节点的prev节点,相当于把root从链表中摘除
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                // root节点的prev节点的next后继节点指向root节点的next节点
                rp.next = rn;
            // first 和 root 节点进行关联,first的前一个节点为root
            if (first != null)
                // 操作与root前后节点关联相同,将first的前一个节点指向root,相当于root目前位于链表的首位
                first.prev = root;
            // 修改root的链表属性,原来的链表头节点现在作为root的下一个节点,变成了第二个节点
            root.next = first;
            // root节点的前驱节点置为空,头节点没有前驱节点
            root.prev = null;
        }
        
        /*
         * 检查红黑树一致性,这一步是防御性的编程
         * 校验TreeNode对象是否满足红黑树和双链表的特性
         * 如果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发场景下);也可能是TreeNode的实现有问题(这个是理论上的以防万一);
         */ 
        assert checkInvariants(root);
    }
}

 7 checkInvariants()

对整棵树进行红黑树一致性的检查,目前仅在检查root是否落在table上时调用。校验是否满足红黑树的特性以及双向链表的特性。

/**
 * Recursive invariant check
 */
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
    TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
        tb = t.prev, tn = (TreeNode<K,V>)t.next;
    //t的前一个节点的next指针应该指向t
    if (tb != null && tb.next != t)
        return false;
    //t的后一个节点的prev指针应该指向t
    if (tn != null && tn.prev != t)
        return false;
    //t的父节点的左子节点或者右子节点应该为t
    if (tp != null && t != tp.left && t != tp.right)
        return false;
    //t的左子节点的父节点应该为t且t的左子节点的has值小于t的哈希值
    if (tl != null && (tl.parent != t || tl.hash > t.hash))
        return false;
    //t的右子节点的父节点应该为t且t的右子节点的hash值应该大于t的hash值
    if (tr != null && (tr.parent != t || tr.hash < t.hash))
        return false;
    //t和t的子节点不能同时为红
    if (t.red && tl != null && tl.red && tr != null && tr.red)
        return false;
    //如果t的左子节点存在,那么左子节点递归检查
    if (tl != null && !checkInvariants(tl))
        return false;
    //如果t的右子节点存在,那么右子节点递归检查
    if (tr != null && !checkInvariants(tr))
        return false;
    return true;
}

 这是一个递归函数。整个执行过程类似于二叉树,叶子节点返回的bool值给上一层,以此类推,一直到了root。

返回上一层后,if (tl != null && !checkInvariants(tl))和if (tr != null && !checkInvariants(tr))会分别判断两个底层返回来的bool值,如果返回的bool值都是true,那么将执行终点return true。


相关文章:【Java集合】HashMap系列(一)——底层数据结构分析
                  【Java集合】HashMap系列(二)——底层源码分析
                  【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值