HashMap源码剖析(二)

上一篇文章hashmap源码剖析的第一部分,由于树化的过程较为复杂,放到此篇来进行详细详解,熟读此篇文章,可以同时掌握TreeMap和LinkedList等数据结构;同样,论述方式按照上一篇文章的风格,注:代码均来自jdk8的hashmap源码
1,详尽注释源码部分;
2,选取部分复杂流程作图解释;
3,总结成述;
从treeifyBin(tab, hash);作为入口开始分析:

// 入参 tab hash桶数组,该hash值所对应的位置需要树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
		// n用来表示数组长度,index表示位置,e用来保存首节点引用
        int n, index; Node<K,V> e;
        // 如果数组为空,或者数组的长度小于最小树化容量,
        // 都只进行扩容优化不进行结构优化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 参照上一篇文章的详解
            resize();
        //   数组长度和当前hash值计算出数组位置,判断该位置是否为空
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        	// 树节点hd用来表示头指针,tl表示尾指针;
            TreeNode<K,V> hd = null, tl = null;
            do {
            	// 这里的replacementTreeNode()方法,
            	// 点进去之后是根据当前节点e创建一个树形节点p
                TreeNode<K,V> p = replacementTreeNode(e, null);
                // 第一次判断尾指针为空
                if (tl == null)
                	// 将当前第一个节点首节点p,作为头指针的引用,hd指向p
                    hd = p;
                else {
                	/* 第二次遍历,尾部指针指向尾部的p此时不为空
                	   将当前节点的前置指针指向上一个循环上一个节点尾部节点,
                	   同时将尾节点的后继指针指向当前节点
                	*/
                    p.prev = tl;
                    tl.next = p;
                }
                // 将尾部指针tl指向当前节点p;
                // 第二轮循环,继续将尾部指针移动到当前节点
                tl = p;
                // e继续向后移动
            } while ((e = e.next) != null);
            // 循环结束,原先的单链表,变成了头指针hd,尾部指针tl的双向链表,
            if ((tab[index] = hd) != null)
            //	此时开始进行由双向链表进行树化
                hd.treeify(tab);
        }
    }

接下来,我们继续分析treeify的源码细节;

final void treeify(Node<K,V>[] tab) {
			// 声明根节点root 
            TreeNode<K,V> root = null;
            // 初始化x为当前的树形节点,双向链表首节点hd
            for (TreeNode<K,V> x = this, next;
             x != null; x = next) {
            	// 保存首节点的后继节点至next节点
                next = (TreeNode<K,V>)x.next;
                // 当前节点x的左右孩子置为空
                x.left = x.right = null;
                // 根为null进入判断
                if (root == null) {
               		/* 当前节点的父节点置为null,
               		且节点颜色置为黑色,x赋值给根节点 */
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                	// 第二次循环进入,获取当前节点的key值
                    K k = x.key;
                	// 获取当前节点的hash值	    
                    int h = x.hash;
                    // 比较器类型kc为null
                    Class<?> kc = null;
                    // 从根节点开始遍历
                    for (TreeNode<K,V> p = root;;) {
                    	// dir来表示key值比较结果;
                    	// ph表示当前节点p的hash值;
                        int dir, ph;
                        // 获取遍历到当前节点p的key值
                        K pk = p.key;
                        // 将当前节点的hash值赋值给ph,如果大于即将插入的
                        // 节点x对应的hash值,那么比较结果为-1
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                        // 如果小于则为1
                            dir = 1;
                        /* 如果比较器为null且比较器返回类型为null
                           或者,计算p节点的key值和x的key的比较结果为0
                        */
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            // 则直接比较两个节点key的hashcode值,得到结果
                            dir = tieBreakOrder(k, pk);
						//	声明xp节点指向当前p节点
                        TreeNode<K,V> xp = p;
                        /* 如果比较结果小于0,那么将当前节点p的左孩子赋值给p
                         否则将右孩子赋值给p,如果p为空,那么待插入的节点父节点
                         直接指向xp节点(p节点赋值之前的节点)
                         */
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            /* 如果比较结果小于0,那么说明key值的比较结果小于当前p节点,
                            那么将该节点置于xp节点的左孩子 */
                            if (dir <= 0)
                                xp.left = x;
                            else
                            // 反之,置于xp的节点的右孩子
                                xp.right = x;
                            // 插入完成后平衡插入,因为红黑树的特点,插入节点后
                            // 要进行高度调整和颜色调整,满足红黑树规则
                            // 下面将继续讨论平衡插入的过程    
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

接下来,我们开始对balanceInsertion(root, x)的过程进行源码分析:
关于红黑树的调整方式,一些前置知识阅读下面的内容,暂时推荐两篇文章
一、红黑树基础描述
二、红黑树调整插入调整
注:第二篇文章有些笔误部分,阅读的时候需要注意下!!!
后续有时间的化,会把红黑树的数据结构以更简化的方式写出来

// 入参1:root为根节点的树,入参2:x插入的节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                          	TreeNode<K,V> x) {
            // 新插入的节点的颜色必须是红色(红黑树插入规则)                                	
            x.red = true;
            // 循环遍历,xp,xpp,xppl,xppr依次表示为x节点的父亲,爷爷,爷爷的左孩子,爷爷的右孩子
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
            	// 将x的父亲节点赋值给xp节点,如果为null的化,说明x为根节点
            	// 那么直接将x节点的颜色变为黑色(红黑树规则),然后返回根节点
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                // 如果父节点是黑色,或者
                // 将父节点的父节点赋值给爷爷节点,爷爷节点为空的化,
                // 那么,直接返回root根节点,即不需要调整树结构
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                // 没有走else if逻辑代表此时xp的颜色已经是红色了
                // 将爷爷节点的左孩子赋值给xppl,如果x的父节点是爷爷节点的左孩子
                // 即x节点的父节点xp是作为其父节点xpp的左孩子    
                if (xp == (xppl = xpp.left)) {
                	// 此时x的父亲节点已经是左孩子,那么继续判断,x的叔叔节点
                	//	不为空并且x的叔叔节点xppr的颜色为红色的化
                    if ((xppr = xpp.right) != null && xppr.red) {
                    // 此时的情况总结为,x为父亲为红色左孩子,x的叔叔为红色右孩子
                    // 且插入的x的颜色为红色
                    // 将叔叔的颜色变为黑色,将父亲的颜色变黑色,将当前x的节点
                    // 继续往上跳到爷爷节点,继续向上调整爷爷的颜色,颜色调整是一个自底向上不断调整的过程
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                    // 进入else逻辑,代表叔叔的节点为黑色,即父亲红,叔叔黑的情况
                        if (x == xp.right) {
                        /* 如果此时,x节点本身是父亲节点的右孩子,
                        那么右父亲为爷爷节点的左孩子,
                        x为父亲节点的有孩子,即左右插入情况 */
                        // 那么进行一次左旋操作,同时将x的节点赋值为父亲节点
                        // 因为左旋之后,x节点和xp节点身份互换,x将成为父节点
                        // xp成为子节点,所以将x赋值成父节点
                            root = rotateLeft(root, x = xp);
                        // 重新将xp的值赋值为x节点的父节点,如果父节点为null
                        // 那么将爷爷节点置为null,否则指向父节点xp的父节点    
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        // 如果父亲节点不为null,先将父节点颜色变黑
                        if (xp != null) {
                            xp.red = false;
                            // 如果爷爷的节点不为空
                            if (xpp != null) {
                            // 将爷爷的颜色变成红色,
                                xpp.red = true;
                            // 然后以爷爷节点开始进行右旋操作,结束第一次遍历调整
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
                	// 此处过程与x的父亲节点为左孩子的处理规则类似,读者可以结合
                	// if逻辑的处理结合判断,加深印象并且做相应的总结
                	// 此处,x的叔叔不为null,且左孩子的颜色为红色,即x的父亲为右孩子,颜色为红色
                    if (xppl != null && xppl.red) {
                    // 此时将叔叔的颜色变黑,父亲的颜色变黑,与上面父亲为左孩子的处理逻辑一样
                        xppl.red = false;
                        xp.red = false;
                        // 爷爷的颜色变红,
                        xpp.red = true;
                        // x继续往上跳到爷爷,接着自底向上开始遍历调整
                        x = xpp;
                    }
                    else {
                    // 此时,父亲为右孩子,如果叔叔为null或者叔叔的颜色为黑色
                    	// 如果x为父亲的左孩子,即父右子左,右左插入
                        if (x == xp.left) {
                        //	那么第一步先将x与父亲节点互换,因为右旋会导致身份互换
                            root = rotateRight(root, x = xp);
                            // 右旋之后,将x的节点父亲重新赋值给xp,完成身份互换
                            // 如果父亲节点为null,那爷爷节点为null,否则爷爷节点为父节点的父节点
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        // 如果x的父亲节点不为null
                        if (xp != null) {
                        	// 那么将父亲节点颜色变为黑色
                            xp.red = false;
                            // 如果爷爷节点存在
                            if (xpp != null) {
                            // 那么将爷爷节点颜色变为红,然后以爷爷节点为中心左旋
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

接下来,将代码里的部分方法的实现继续深入分析,首先是左旋操作rotateLeft(root, xpp),右旋操作与此类似不作分析,如下:

// 入参1:待旋转操作的树 入参2:旋转中心点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            // 声明树节点r表示旋转点的右孩子,pp代表                                  
            TreeNode<K,V> r, pp, rl;
            // 旋转点p不为null,且p的右孩子赋值给r也不为null
            if (p != null && (r = p.right) != null) {
            	// 将旋转点p的右孩子的左孩子赋值给旋转点p的右孩子赋值给rl,rl不为空
                if ((rl = p.right = r.left) != null)
                	// 那么将p节点的右孩子的左孩子rl的父节点指向旋转点p
                    rl.parent = p;
                // 如果旋转点的父节点赋值给 右孩子的父节点(旋转后r将成为父节点,要保留旋转前p节点的父节点引用)    
                // 然后将旋转前的p节点的父节点应用保存到pp,如果pp节点为null
                if ((pp = r.parent = p.parent) == null)
                	// 说明旋转点为根节点,那么将右孩子赋值给root根节点
                	// 且颜色变为黑色(红黑树规则)
                    (root = r).red = false;
                // 如果不为null,且旋转点为左孩子
                else if (pp.left == p)
                	// 那么将旋转之后的r作为pp节点的左孩子
                    pp.left = r;
                else
                	// 反之,将旋转之后的r作为pp节点的右孩子
                    pp.right = r;
                // 旋转之后,将原先的旋转点p作为新的中心点r的左孩子    
                r.left = p;
                // 并且将左孩子p的父节点指向r
                p.parent = r;
            }
            // 返回旋转后的root
            return root;
        }

左旋转小结,红黑树里面节点,每个节点保留着对父节点,和左右孩子的引用,左旋的时候,将旋转点的右孩子置为中心点,将右孩子的左孩子作为旋转点的右孩子,将旋转点作为中心点的左孩子,同时更改各个节点的父节点以及子节点的引用。下面以简单的图作为示例进行展示:
左旋过程
分析完balanceInsertion()方法,继续回到treeify方法的部分,最后一个moveRootToFront(tab, root);该方法的实现比较简单,即保证形成的红黑树的根节点为hashmap数组的该位置的首节点,即tab[i]与root相等;

总结:
1、hashmap的树化过程,借用来红黑树,此篇大致介绍来红黑树的插入调整和颜色调整规则;
2、插入调整的流程可以总结为:
2-1、父节点黑,直接插入不需要调整;
2-2、父节点为红色,以父亲节点左子节点分析

1). 叔叔节点为红色,那么只需要通过变色来调整即可完成红黑树规则调整,此时将父亲和叔叔变黑,然后跳至爷爷节点,继续向上遍历调整;
2). 叔叔节点为黑色
2-2-1、如果给插入节点为左子节点,那么将父亲变黑,爷爷变红然后以爷爷节点进行右旋操作;
2-2-2、如果插入节点为父亲节点右子节点,那么先左旋,然后再将父亲变黑,爷爷变红,以爷爷节点进行右旋操作;

3、父亲节点为红色,父亲节点为右子节点,与第二种情况类似,调整方向相反,读者可以自行分析,以加深印象;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值