注意:这里没有讨论变成红黑树,临界值8的情况
扩容机制 final Node<K,V>[] resize() { // 定义一个Node[] Node<K,V>[] oldTab = table; // 老的容量 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; } // 每次扩容都是前一次的两倍,阈值也是 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 这里是最开是初始化的时候,默认容量是16,默认的阈值时0.75*16=12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; ……#############这下面才是扩容机制中重新确定每个Node节点所在的位置的精髓所在,单独讲################# }为什么每次扩容设置成前一次的2倍?
我们再putVal方法中看到,HashMap确定数组存储的下标是这样确定的:(table.length-1) & hash,与运算是二进制的运算,都是1则为1,否则为0。
因为table的容量总是2的倍数,所以换成二进制时全部都是1后面都是0,比如16,二进制就是10000,32就是100000,那么减一之后它的二进制就都会是1,比如15的二进制是1111,31的二进制11111,这样再与hash值做与运算的时候离散性会更好,降低hash碰撞
为什么hash计算需要右移16位做异或运算
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }首先需要确定的一点是,用hash值是为了快速检索定位,但是需要好的hash算法去减少hash冲突,提高离散性。以这点作为基础,猜想上面这样算法是因为降低hash冲突,提高离散性。
(table.length-1) & hash还是再看确定bin下表的方式,table的长度和hash值做与运算,在实际中table的长度并不会特别大,2的16次方都比较少,更何况是32位,所以如果直接用hashCode()方法生成的hash值做运算,其实大概率只用到了后16位,前十六位就浪费了。所以(h = key.hashCode()) ^ (h >>> 16)先用右移16位做异或运算,其实就是后16位和前16位做异或运算,这样在确定下标中hash的32位都参与了运算,这样既就增加了随机性,提高了离散性。
为什么是做"异或"运算呢?因为相比于“与”运算和“或”运算,“异或”运算生成0和1的概率是相等的,没有偏重哪一个。
@SuppressWarnings({"rawtypes","unchecked"}) // 确定完容量后,new一个新的数组,将新的数组赋给table这个数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 这里就是来确定老的table中每个桶中每个Node在新的table中的位置是否要移动,怎么移动 // 这里如果不明白为什么需要在新表中重新确定位置的,看下面的图解可能好理解 for (int j = 0; j < oldCap; ++j) { // 临时节点e Node<K,V> e; // 取出第j个位置的节点给e if ((e = oldTab[j]) != null) { oldTab[j] = null; // 这说明在这个原来的j位置上不存在hash碰撞,就直接放到新table中相同的位置就行 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 // 进到这个else就说明在原来j位置存在hash碰撞,形成链表了,e.next不为null // 低位节点,loHead地位节点的head,loTail地位节点的尾部 Node<K,V> loHead = null, loTail = null; // 高位节点 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 循环遍历这个桶的链表的每个节点,重新弄确定位置 do { next = e.next; // 这个条件的&运算非常巧妙,如果为0,就说明这个节点不需要移动位置,在新table中也是j这个位置的桶中 // 这里这个&运算的结果不为0就为1,下面详细介绍 if ((e.hash & oldCap) == 0) { // 这里是理清节点的关系,相当于这里是这个桶中所有不需要移位的Node,又要形成一个新的链表 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; // 这里就是低位节点,不需要移位,在新的table中还是j位置 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // 高位节点在新的table中的位置就变成 (原来的位置+原来的容量) newTab[j + oldCap] = hiHead; } } } } } return newTab;高位低位节点说明:
上面的代码中 e.hash & oldCap 这个结果为什么不是0就是1呢?
首先要明白为什么多个Node会存到同意的桶中,也就是在寻找数组位置的时候找到了同一个。