Java 1.8中HashMap的resize()方法擴容部分的理解
阿新 • • 發佈:2018-12-23
首先可以看這篇文章,對擴容前面部分進行了解:Java 8系列之重新認識HashMap
###沒有別的說的,程式設計師直接看程式碼。
紅黑樹比較麻煩,直接刪除了。
// 擴容兼初始化 final Node<K, V>[] resize() { 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) { // 原陣列長度大於最大容量(1073741824) 則將threshold設為Integer.MAX_VALUE=2147483647 // 接近MAXIMUM_CAPACITY的兩倍 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { // 新陣列長度 是原來的2倍, // 臨界值也擴大為原來2倍 newThr = oldThr << 1; } } else if (oldThr > 0) { // 如果原來的thredshold大於0則將容量設為原來的thredshold // 在第一次帶引數初始化時候會有這種情況 newCap = oldThr; } else { // 在預設無引數初始化會有這種情況 newCap = DEFAULT_INITIAL_CAPACITY;// 16 newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 0.75*16=12 } if (newThr == 0) { // 如果新 的容量 ==0 float ft = (float) newCap * loadFactor;// loadFactor 雜湊載入因子 預設0.75,可在初始化時傳入,16*0.75=12 可以放12個鍵值對 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; // 如果原來的table有資料,則將資料複製到新的table中 if (oldTab != null) { // 根據容量進行迴圈整個陣列,將非空元素進行復制 for (int j = 0; j < oldCap; ++j) { Node<K, V> e; // 獲取陣列的第j個元素 if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果連結串列只有一個,則進行直接賦值 if (e.next == null) // e.hash & (newCap - 1) 確定元素存放位置 newTab[e.hash & (newCap - 1)] = e; // 此處省略紅黑樹 else { // 進行連結串列複製 // 方法比較特殊: 它並沒有重新計算元素在陣列中的位置 // 而是採用了 原始位置加原陣列長度的方法計算得到位置 Node<K, V> loHead = null, loTail = null; Node<K, V> hiHead = null, hiTail = null; Node<K, V> next; do { /*********************************************/ /** * 注: e本身就是一個連結串列的節點,它有 自身的值和next(連結串列的值),但是因為next值對節點擴容沒有幫助, * 所有在下面討論中,我近似認為 e是一個只有自身值,而沒有next值的元素。 */ /*********************************************/ next = e.next; // 注意:不是(e.hash & (oldCap-1));而是(e.hash & oldCap) // (e.hash & oldCap) 得到的是 元素的在陣列中的位置是否需要移動,示例如下 // 示例1: // e.hash=10 0000 1010 // oldCap=16 0001 0000 // & =0 0000 0000 比較高位的第一位 0 //結論:元素位置在擴容後陣列中的位置沒有發生改變 // 示例2: // e.hash=17 0001 0001 // oldCap=16 0001 0000 // & =1 0001 0000 比較高位的第一位 1 //結論:元素位置在擴容後陣列中的位置發生了改變,新的下標位置是原下標位置+原陣列長度 // (e.hash & (oldCap-1)) 得到的是下標位置,示例如下 // e.hash=10 0000 1010 // oldCap-1=15 0000 1111 // & =10 0000 1010 // e.hash=17 0001 0001 // oldCap-1=15 0000 1111 // & =1 0000 0001 //新下標位置 // e.hash=17 0001 0001 // newCap-1=31 0001 1111 newCap=32 // & =17 0001 0001 1+oldCap = 1+16 //元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化: //參考博文:[Java8的HashMap詳解](https://blog.csdn.net/login_sonata/article/details/76598675) // 0000 0001->0001 0001 if ((e.hash & oldCap) == 0) { // 如果原元素位置沒有發生變化 if (loTail == null) loHead = e;// 確定首元素 // 第一次進入時 e -> aa ; loHead-> aa else loTail.next = e; //第二次進入時 loTail-> aa ; e -> bb ; loTail.next -> bb;而loHead和loTail是指向同一塊記憶體的,所以loHead.next 地址為 bb //第三次進入時 loTail-> bb ; e -> cc ; loTail.next 地址為 cc;loHead.next.next = cc loTail = e; // 第一次進入時 e -> aa ; loTail-> aa loTail指向了和 loHead相同的記憶體空間 // 第二次進入時 e -> bb ; loTail-> bb loTail指向了和 loTail.next(loHead.next)相同的記憶體空間 loTail=loTail.next // 第三次進入時 e -> cc ; loTail-> cc loTail指向了和 loTail.next(loHead.next.next)相同的記憶體 } else { //與上面同理 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null);//這一塊就是 舊連結串列遷移新連結串列 //總結:1.8中 舊連結串列遷移新連結串列 連結串列元素相對位置沒有變化; 實際是對物件的記憶體地址進行操作 //在1.7中 舊連結串列遷移新連結串列 如果在新表的陣列索引位置相同,則連結串列元素會倒置 if (loTail != null) { loTail.next = null;// 將連結串列的尾節點 的next 設定為空 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null;// 將連結串列的尾節點 的next 設定為空 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
擴容中在新陣列中存放元素的流程圖解:
注: e本身就是一個連結串列的節點,它有 自身的值和next(連結串列的值),但是因為next值對節點擴容沒有幫助,所有在下面討論中,我近似認為 e是一個只有自身值,而沒有next值的元素。
在每次擴容前,e的next值有專門的next變數接收(next = e.next)
注:圖中aa,bb,cc 代指元素在儲存的地址