转载

Hashmap源码解析-扩容函数

1.更新容量和阈值,

​ if cap>0, 不超过上限的情况下cap、thre都乘2

​ if cap=0, oldThr>0, 说明初始化的时候赋初始容量参数了,newCap=oldThr

​ if cap=0, oldthr=0, 直接重新初始化,cap=16,thre=12

2.更新哈希桶, 遍历原桶

​ if 只有一个节点,直接挪过去

​ if 链表有超过8个节点,是红黑树, 复杂, 再说todo

​ if 少于8个的链表,则可能挪到低位,也可能挪到高位,看它本身hash在新容量时应在哪里,代码中巧妙通过与oldCap & 的方式判断需改到高还是低,具体在代码注释中有

final Node<K,V>[] resize() {

    //oldTab 为当前表的哈希桶

    Node<K,V>[] oldTab = table;

    //当前哈希桶的容量 length

    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //当前的阈值

    int oldThr = threshold;

    //初始化新的容量和阈值为0

    int newCap, newThr = 0;

    //如果当前容量大于0

    if (oldCap > 0) {

        //如果当前容量已经到达上限

        if (oldCap >= MAXIMUM_CAPACITY) {

            //则设置阈值是2的31次方-1

            threshold = Integer.MAX_VALUE;

            //同时返回当前的哈希桶,不再扩容

            return oldTab;

        }//否则新的容量为旧的容量的两倍。 

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                    oldCap >= DEFAULT_INITIAL_CAPACITY)   //如果旧的容量大于等于默认初始容量16

            //那么新的阈值也等于旧的阈值的两倍

            newThr = oldThr << 1; // double threshold

    }//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况

    else if (oldThr > 0) // initial capacity was placed in threshold

        newCap = oldThr;//那么新表的容量就等于旧的阈值

    else {     //如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况               // zero initial threshold signifies using defaults

        newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量16 * 默认加载因子0.75f = 12

    }

    if (newThr == 0) {//如果新的阈值是0,对应的是  当前表是空的,但是有阈值的情况

        float ft = (float)newCap * loadFactor;//根据新表容量 和 加载因子 求出新的阈值

        //进行越界修复

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                    (int)ft : Integer.MAX_VALUE);

    }

    //更新阈值 

    threshold = newThr;

    @SuppressWarnings({"rawtypes","unchecked"})

    //根据新的容量 构建新的哈希桶

        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

    //更新哈希桶引用

    table = newTab;

    //如果以前的哈希桶中有元素

    //下面开始将当前哈希桶中的所有节点转移到新的哈希桶中

    if (oldTab != null) {

        //遍历老的哈希桶

        for (int j = 0; j < oldCap; ++j) {

            //取出当前的节点 e

            Node<K,V> e;

            //如果当前桶中有元素,则将链表赋值给e

            if ((e = oldTab[j]) != null) {

                //将原哈希桶置空以便GC

                oldTab[j] = null;

                //如果当前链表中就一个元素,(没有发生哈希碰撞)

                if (e.next == null)

                    //直接将这个元素放置在新的哈希桶里。

                    //注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高

                    newTab[e.hash & (newCap - 1)] = e;

                    //如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树(暂且不谈 避免过于复杂, 后续专门研究一下红黑树)

                else if (e instanceof TreeNode)

                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                //如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。

                else { // preserve order

                    //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位=  low位+原哈希桶容量

                    //低位链表的头结点、尾节点

                    Node<K,V> loHead = null, loTail = null;

                    //高位链表的头节点、尾节点

                    Node<K,V> hiHead = null, hiTail = null;

                    Node<K,V> next;//临时节点 存放e的下一个节点

                    do {

                        next = e.next;

                        // 此处操作跟hash计算索引有关

                        // 在 HashMap 中,索引的计算方法为 (n - 1) & hash

                        // 所以,在进行扩容操作 (n*2) 后,该计算结果可能导致变更

                        // 例如

                        // 有一个值为 111001 的 hash

                        // 扩容前 n=16(10000)  n-1=15(1111)  (n - 1) & hash = 1111 & 111001= 001001

                        // 扩容后 n=32(100000) n-1=31(11111) (n - 1) & hash = 11111 & 111001= 011001

                        // 假如 hash 值为 101001

                        // 那么会发现扩容前  1111 & 101001 = 001001

                        //           扩容后 11111 & 101001 = 001001

                        // 所以可知,在进行扩容操作时,主要按照 hash 与 原数组长度中1的对应位置有关

                        // 如果 hash 中对应的位置为0,扩容后索引结果不变

                        // 不为0,表示索引结果为原结果+原数组长度

                        // 而 hash 中该对应位置的值只存在俩种可能 0,1

                        // 所以在该节点中的数据大约有一半索引不变,一半为原索引+原数组长度

                        // 通过 e.hash & oldCap 的方式可以得知 hash 在 oldCap 1对应的位置是否为0或1

                        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);

                    //将低位链表存放在原index处,

                    if (loTail != null) {

                        loTail.next = null;

                        newTab[j] = loHead;

                    }

                    //将高位链表存放在新index处

                    if (hiTail != null) {

                        hiTail.next = null;

                        newTab[j + oldCap] = hiHead;

                    }

                }

            }

        }

    }

    return newTab;

}
原文  https://techwei.cn/2019/05/26/hashmap/扩容函数/
正文到此结束
Loading...