Java 1.8 HashMap扩容原理

1. HashMap源码讲解

源码讲解网上太多了,能够参考HashMap此生来世。 我这里主要讲解Java 8的HashMap扩容原理。下文中声明的桶与数组的含义是一致的html

1. 先介绍hash()方法,之因此不用Object自带的hashCode方法是怕别人伪造相同hash值的key,使得哈希冲突。该方法注释上说了一大堆,意思大概是为了减小哈希冲突,让高16位参与运算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码

2. 再来介绍一下HahsMap是如何定位到桶的位置?

i = (tab.length - 1) & hash
复制代码

这与咱们平时学的hash表算法不太同样,先说效果:位运算计算快,效率高,与数组长度取模(好比Hashtabal的put方法就是哈希取模)效果同样。 由于桶的个数是2的n次幂,以默认16举例。tab.length -1 =15,二进制就是 1111,与hash进行&运算,获得的结果是hash值的最后4位。运算规则参考我上一篇文章算法

3. 终于来到了扩容部分,咱们以桶个数默认16,扩容到32为例子,直接看源码。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // oldCap = 16
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // newCap =oldCap << 1 = 32
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        ···// 省略其余代码,下面就是扩容代码,数据怎么转移到新数组newTab中
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // oldCap是旧桶的个数,也就是没扩容以前数组的长度,这里等于16 。这里遍历桶
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; //清除桶元素,利于回收
                    // 若是桶中元素后面没有其余元素,直接计算新index,插入新桶中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //  若是桶中元素还有下一个元素,节点类型是TreeNode类型,那么按照TreeNode方式插入。这个比较复杂    
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 到了这个分支,就说明节点类型是链表,咱们主要分析这个
                    else { 
                    // preserve order 
                    // 3.1 
                        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;
                            // j + oldCap是由于高位,正好是2的4次方,也就是oldCap的长度
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

复制代码

3.1

这里注释preserve order说明了下面链表的迁移保持顺序,让我想起来Java 1.7的HashMap在扩容的时候,全部元素所有从新计算Hash&(NewTable.length-1),致使扩容后的链表没有保持顺序,因此在并发的Rehash状况下会致使死循环shell

来看看核心判断条件(e.hash & oldCap) == 0。这里oldCap=16,就是Hash值与10000进行或运算(只有两个数都为1,结果才为1)。计算结果也就是判断Hash值的第5个bit位是否等于0。这里为何要判断第5个bit位呢?由于以前计算桶下标的时候是直接取的hash值的前4位,若是扩容后,计算t桶下标方式就是(hash & (newCap - 1)) ,newCap =32,也就是hash & (100000-1),也就是与11111进行或运算,获得的结果就是hash值的前5位。看到这里不知道你是否有点迷糊,那就看个图。数组

因此判断条件(e.hash & oldCap) == 0 计算出来的值,若是等于0,就放入低位节点,低位节点在新数组的位置跟原数组同样。 等于1,就放入高位节点,高位放在新数组的位置等于原来位置在加上原数组的长度,由于这个1是计算出来的第5个bit位,因此是10000,也就是2的4次方,16。正好等于原来数组长度。

看到这里,你会发现这个设计很是的巧妙,避免了所有从新Rehash的过程,而且新增的bit为能够认为是随机的,这样子就能够将以前冲突的数据分散到新的桶中。bash

参考部分:并发

HashMap今世前身ide

Jdk1.8中的HashMap实现原理post

相关文章
相关标签/搜索