HashMap浅析(二)

HashMap浅析(二)

扩容机制

首先上扩容的代码,每一行都写了注释,尽可能的让大家明白这个过程。

final Node<K,V>[] resize() {
    	//将table赋值给oldTab
        Node<K,V>[] oldTab = table;
    	//声明oldTab的容量给oldCap
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	//扩容阈值赋值给oldThr
        int oldThr = threshold;
    	//上面这三行代码,其实就是为了区分出扩容前与扩容后的变量,众所周知,数组无法动态扩容
    	//声明新数组的容量和扩容阈值,暂时为0
        int newCap, newThr = 0;
    	//如果旧数组的容量大于0
        if (oldCap > 0) {
            //如果旧数组的容量大于等于最大值,即1<<30
            if (oldCap >= MAXIMUM_CAPACITY) {
                //全局变量的扩容阈值赋值为int的最大值,同时返回旧数组
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果新数组的容量(旧数组容量*2)小于最大容量,且,旧数组容量大于等于默认值1<<4
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新数组的扩容阈值等于旧数组扩容阈值的2倍
                newThr = oldThr << 1; // double threshold
        }
    	//如果旧数组的扩容阈值大于0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //新数组的扩容阈值就等于旧数组的扩容阈值
            newCap = oldThr;
    	//如果既不走oldCap>0,也不走oldThr>0的分支,那么走else
        else {               // zero initial threshold signifies using defaults
            //新数组的容量为默认容量:16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //新数组的扩容阈值=默认载荷因素*默认初始容量=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	//新的if分支:如果新数组的扩容阈值等于0
        if (newThr == 0) {
            //ft=新数组容量*0.75,loadFactory在用户未指定时,就是默认的0.75
            float ft = (float)newCap * loadFactor;
            //当新数组的容量小于最大容量,且ft小于最大容量时,新数组的扩容阈值就为ft,
            //否则为int的最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    	//将新得到的新数组的扩容阈值,赋值给全局变量的扩容阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
    	//创建一个长度为newCap的node数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    	//并让全局变量的table数组指向新数组
        table = newTab;
    	//如果旧数组不等于空
        if (oldTab != null) {
            //循环旧数组
            for (int j = 0; j < oldCap; ++j) {
                //声明一个Node对象,用来承载旧值
                Node<K,V> e;
                //将数组的j角标位置的值赋值给e,同时判断是否为null
                if ((e = oldTab[j]) != null) {
                    //不为空,则把原数组的j位置重新赋值为null
                    //当循环完毕,旧数组就是空数组,方便回收
                    oldTab[j] = null;
                    //判断当前数组元素e的链表的下一个结点是否为null
                    if (e.next == null)
                        //证明当前数组元素只有一个结点,所以只需在新数组中放置e元素
                        newTab[e.hash & (newCap - 1)] = e;
                    //首先走到else if分支这里,已经确定e.next不为null
                    //那么就需要判断是红黑树还是链表
                    else if (e instanceof TreeNode)
                        //红黑树部分先过掉
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //确定为链表之后
                    else { // preserve order
                        //创建五个node对象
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //注意是do-while循环,即先执行一次,再走while
                        do {
                            //next即为当前遍历到的元素的下一个结点
                            next = e.next;
                            //如果当前元素的hash&旧数组容量的值为0
                            //当你看到这里时,请先看下一节:扩容中链表复制的详解
                            //便于理解之后的流程
                            //判断角标是否需要修改,下面的分析会单独拿出来,因为涉及到循环和判断
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else 
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

do-while中具体执行流程

现在假设当前元素e的链表中存在三个结点,a、b、c分别为三个结点的内存地址,假设e不需要重新计算角标

也就是e ->a, e.next->b ,e.next.next->c

在这里插入图片描述

第一次执行

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //第一次执行,next->b,e->a
                            next = e.next;
                            //判断元素位置是否改变
                            if ((e.hash & oldCap) == 0) {
                                //不需要改变走这个分支
                                //判断loTail是否为null,现在是第一次执行,肯定是null,进入if分支
                                if (loTail == null)
                                    //loHead->a
                                    loHead = e;
                                else
                                    loTail.next = e;
                                //loTail->a
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //然后执行判断的同时,将e->next,e->b
                        } while ((e = next) != null);

用个图表示第一次执行之后,变量的关系

在这里插入图片描述

第二次循环

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //next->c,e->b
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                //loTail->a,所以走else
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //loTail.next->b
                                    loTail.next = e;
                                //loTail->b
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //然后执行判断的同时,将e->next,e->c
                        } while ((e = next) != null);

在这里插入图片描述

第三次循环

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //next->null,e.next->null,e->c
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //loTail.next->c
                                    loTail.next = e;
                                //loTail->c
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //e=next == null,while循环结束
                        } while ((e = next) != null);

在这里插入图片描述

跳出循环之后的处理

						//loTail不为null,因为loTail指向的是a的内存地址,而a又是链表的头结点
						//所以只需要将新数组的j位置元素指向loHead的地址,最终也就是指向的a
						if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
						//这个方法是针对需要重新计算元素下角标的元素
                        if (hiTail != null) {
                            hiTail.next = null;
                            //针对为何是j+oldCap可以查看下面的内容
                            newTab[j + oldCap] = hiHead;
                        }

扩容中链表复制的详解

在resize()方法中,针对链表的复制,HashMap采取了一个非常有意思的方法。

它没有针对新数组重新计算hash,从而确定下角标,而是通过e.hash & oldCap判断元素的前后位置是否有变化。

  1. 假设当前元素的hash为10:1010(二进制),数组扩容前为8:1000(二进制),

    那么计算角标的方法就是(8-1)&10,index=2

    在这里插入图片描述

  2. 扩容后的下角标,hash为10:1010(二进制),数组扩容后为16:0001 0000(二进制),

    那么计算角标的方法就是(16-1)&10,index=10

    在这里插入图片描述

大家仔细看,其实扩容就是高一位从0到1,这时如果确定hash的对应位是0还是1,就可以确定,元素在新数组的位置是否有变化了。

在这里插入图片描述

那么如何确定呢,e.hash & oldCap就是用来判断这个变化的。

首先&的运算,就保证了如果两个二进制的对应位都是1,计算出来的值的对应位才为1.

在这里插入图片描述

由上图可知,e.hash & oldCap!=0,所以需要重新计算位置。

通过下图,你能发现什么问题呢?

新旧座标的差值其实就是数组扩容前后,区分出来的高一位1,然后又因为数组扩容是严格按照2的幂次方进行的,所以必然存在:

当e.hash & oldCap!=0时,

新数组的下角标位置=旧数组的下角标+2的m次方(m=图中容量扩容前后-1的二进制码中的发生变化的位置)
newIndex=oldIndex+2m newIndex=oldIndex+2^m

2m=oldCap 2^m=oldCap
所以有
newIndex=oldIndex+oldCap newIndex=oldIndex+oldCap

在这里插入图片描述

如果还不够明白的话,我继续通过推导e.hash & oldCap==0的情况给各位再反向操作一波。

为了方便推导,我将其中的hash值修改了一下,下图是扩容前后求index的图。

下图可知:index值在扩容前后没有发生变化。

在这里插入图片描述

下图是e.hash & oldCap的计算图,对应图中标注的1.2.3,依次做一个解释。

1.因为容量是严格按照2的幂次方扩容,所以整段二进制码中,只存在一个1;

2.两者进行"&"计算后,如果hash的对应位(指容量的二进制码中1的位置)为0,那么结果必然为0.

3.反之亦然。

当扩容后的容量为16, 0001 0000,

所对应的n-1,即16-1, 0000 [1]111,

旧容量的n-1,即8-1, 0000 [0]111,

二者的区别就在[]号标识的那一位,所以利用上面2.的结论,无论是扩容多少次,满足e.hash & oldCap==0的条件下,角标都不会变

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章