Java8 HashMap扩容时为什么不需要重新hash

 

技巧: 与&操作   和   与 n 如8 与,为0  则位置不变

https://blog.csdn.net/zlp1992/article/details/104376309

java8在实现HashMap时做了一系列的优化,其中一个重要的优化即在扩容的时候,原有数组里的数据迁移到新数组里不需要重新hash,而是采用一种巧妙的方法,代码如下:

        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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;
    }

 

可以看到它是通过将数据的hash与扩容前的长度进行与操作,根据结果为0还是不为0来做对应的处理e.hash & oldCap
举个例子
比如数据 A它经过hash之后的值为 0111(二进制),一开始map中数组的长度是 8,根据定位的逻辑 (n-1)&hash,那么数据A的位置是:

(n-1)&hash = (8-1) & 0111 = 111 & 0111 = 0111

扩容之后数组长度是原来的2倍,即16,假设我们重新算一遍A的位置,那么应该是:

(n-1)&hash =(16-1) & 0111 = 1111 & 0111 = 0111

可以看到数据A扩容之后,如果重新计算hash的话,它的位置是没有发生变化的,来看一下 e.hash & oldCap的结果

e.hash & oldCap = 0111 & 8 = 0111 & 1000 = 0

比如数据B它经过hash之后的值为 1111,在扩容之前数组长度是8,数据B的位置是:

(n-1)&hash = (8-1) & 1111 = 111 & 1111 = 0111

扩容之后,数组长度是16,重新计算hash位置是:

(n-1)&hash = (16-1) & 1111 = 1111 & 1111 = 1111

可见数据B的位置发生了变化,同时新的位置和原来的位置关系是:
新的位置(1111)= 1000+原来的位置(0111)=原来的长度(8)+原来的位置(0111)
继续看一下e.hash & oldCap的结果

e.hash & oldCap = 1111 & 8 = 1111 & 1000 = 1000 (!=0)

那么有没有看出规律呢,因为每次扩容都是2的倍数,计算位置的时候是和数组的长度-1做与操作,那么影响位置的数据只有最高的一位,比如 8-1 =7= 0111 ,16-1=15=1111 ,对于每个数据来说只有从右边数第四位的值会影响结果,当数据的hash的右边第四位为1的时候位置会发生变化,如上面的数据B,如果第四位为0,那么数据不会发生变化,如上面的数据A,而这个第四位 1000 恰好又是扩容前的数组长度,因此可以根据e.hash & oldCap的结果来判断,如果是0,说明位置没有发生变化,如果不为0,说明位置发生了变化,而且新的位置=老的位置+老的数组长度。
不过为什么当某个bin只有一个数据的时候要重新hash呢?

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

理论上也可以采用e.hash & oldCap这种方式?

posted @ 2021-02-20 23:19  abcdefghijklmnop  阅读(226)  评论(0编辑  收藏  举报