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