HashMap的resize()即擴容方法源碼解讀(已重新完善,如有不足之處,歡迎指正~)

分析HashMap的resize()即擴容方法的源碼,會發現主要分兩部分操作:

  1. 爲創建新數組初始化新數組容量和新數組擴容閾值;
  2. 創建新數組後,需將數據從舊數組轉移到新數組上來,舊數組上的數據會根據(e.hash & oldCap) 是否等於0,重新rehash計算其在新數組上的索引位置,分成2類:
    ① 等於0時,則將該頭節點放到新數組時的索引位置等於其在舊數組時的索引位置,記未低位區鏈表lo開頭-low;
    ② 不等於0時,則將該頭節點放到新數組時的索引位置等於其在舊數組時的索引位置再加上舊數組長度,記爲高位區鏈表hi開頭high(具體推導過程,詳見《HashMap擴容時的rehash方法中(e.hash & oldCap) == 0算法推導》).
    具體,詳見下述的源碼解析:
    /HashMap的resize()方法/
    /
    *
    • Initializes or doubles table size. If null, allocates in

    • accord with initial capacity target held in field threshold.
      *初始化或者加倍數組長度.若未指定要擴的容量值,則按照字段threshold所持有的初始容量目標分配.

    • Otherwise, because we are using power-of-two expansion, the

    • elements from each bin must either stay at same index, or move

    • with a power of two offset in the new table.
      *另外,因爲我們使用的數組長度是2的n次冪的格式,當擴容後,數組原來的每個桶中的元素要麼保存在原位置,要麼相比舊數組,擴容到新數組後,位置的偏移量爲2的n次冪。

    • @return the table
      /
      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) {//如果舊數組容量>0
      if (oldCap >= MAXIMUM_CAPACITY) {//如果舊數組容量大於等於最大容量
      threshold = Integer.MAX_VALUE; //則直接修改舊數組擴容閾值爲最大值
      return oldTab; //並返回舊數組容量,不再做其他操作
      }
      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)//若舊數組容量小於最大容量且新數組容量擴大至舊數組容量的2倍後依舊小於最大容量,並且//舊數組容量大於等於默認的初始化容量16
      newThr = oldThr << 1; // double threshold //則將新數組擴容閾值擴大至舊數組擴容閾值的2倍
      }
      else if (oldThr > 0) // initial capacity was placed in threshold//若舊數組容量小於等於0,且舊數組擴容閾值大於0(當new HashMap(0)後再put時,會走到這裏)
      newCap = oldThr; //則將舊數組擴容閾值賦給新數組容量
      else {// zero initial threshold signifies using defaults//若舊數組容量和舊數組擴容閾值均不大於0,說明數組需要初始化
      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;
      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
      (int)ft : Integer.MAX_VALUE);
      }
      threshold = newThr; //將新數組擴容閾值賦值給HashMap的擴容閾值字段
      @SuppressWarnings({“rawtypes”,“unchecked”})
      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //按照新數組容量創建新數組
      table = newTab; //將創建的新數組賦值給HashMap的數組字段
      if (oldTab != null) {//若舊數組不爲null,則需要將舊數組中的數組遷移到新數組中,並將舊數組各位置置爲null.
      for (int j = 0; j < oldCap; ++j) {//根據舊數組長度,循環遍歷各數組索引下標
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {//判斷每個數組索引位置對應的鏈表的頭節點是否爲空,若爲空則該索引位置無數據,就不需要接下來的操作,不爲空才繼續往下進行處理,將該鏈表的數據轉移賦值給新數組
      oldTab[j] = null; //將舊數組該位置置爲null,提醒gc回收
      if (e.next == null) //頭節點無後續節點,說明只需將頭節點移動到新數組
      newTab[e.hash & (newCap - 1)] = e; //根據新數組長度和該鏈表頭節點已有的hash重新計算該鏈表頭節點在新數組中的索引下標位置,並將頭節點直接賦值給新數組的該索引下標。
      else if (e instanceof TreeNode) //判斷鏈表頭節點類型是否是紅黑樹
      ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //將樹中的數據從舊數組移到新數組(此方法解讀詳見:《HashMap中紅黑樹TreeNode的split方法源碼解讀》)
      else { // preserve order//走到這裏,說明鏈表頭節點有後續節點,後面會保留原有鏈表的順序進行新舊數組的數據轉移
      //舊數組的每個索引位置裏的鏈表頭節點轉移到新數組後的索引位置是要重新rehash,重新計算的:根據(e.hash & oldCap) 是否等於0,分成2類:①等於0時,則將該頭節點放到新數組時的索引位置等於其在舊數組時的索引位置,記未低位區鏈表lo開頭-low;②不等於0時,則將該頭節點放到新數組時的索引位置等於其在舊數組時的索引位置再加上舊數組長度,記爲高位區鏈表hi開頭high(具體推導過程,詳見《HashMap擴容時的rehash方法中(e.hash & oldCap) == 0源碼解讀》)。
      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) {//詳見《HashMap擴容時的rehash方法中(e.hash & oldCap) == 0源碼解讀》:用鏈表節點的hash值與爲2的n次冪的舊數組長度直接進行與的位運算,若(e.hash & oldCap)的結果爲0,則可以推導得到該鏈表節點所在的鏈表頭節點移動到擴容爲2倍的新數組時的所在索引下標位置與在舊數組的索引下標位置相同(處於同一條鏈表中的所有節點的hash值相同):(2oldCap -1) & e.hash=(oldCap -1) & e.hash.[注意:1.舊數組長度oldCap爲2的n次冪;2.計算某鏈表節點e在長度爲n的數組中的索引下標的公式爲(n-1)&e.hash;3.數據e的hash是根據e的key計算得到的,公式爲(h=key.hashCode()) ^ (h>>>16)].
      if (loTail == null) //低位鏈表的末尾爲null, 對於每個鏈表來說,說明是第一次走到這裏,而且此處也只會走進來一次,因爲後續會將非null的e賦值給loTail了。
      loHead = e; //說明e爲低位鏈表頭節點,並將其賦給代表低位鏈表頭節點的loHead
      else//說明低位鏈表末尾不爲null,說明至少處理過一次loTail了,即頭節點肯定已經處理過了,下面應該去處理低位鏈表頭節點的後續節點了
      loTail.next = e; //處理完低位鏈表頭節點後,根據next=e.next和while((e=next)!=null),依次循環遞進處理低位鏈表頭節點的後續節點,將舊數組中的鏈表頭節點的後續節點,追加到低位鏈表頭節點loHead的next裏。
      loTail=e; //將非null的e賦給loTail,首次走到這裏時,loTail和loHead都指向e。
      }
      else {//詳見《HashMap擴容時的rehash方法中(e.hash & oldCap) == 0源碼解讀》: 用鏈表節點的hash值與爲2的n次冪的舊數組長度直接進行與的位運算,若(e.hash & oldCap)的結果不爲0,則可以推導得到該鏈表節點所在的鏈表頭節點移動到擴容爲2倍的新數組時的所在索引下標位置與在(舊數組的索引下標位置+舊數組長度)相等(處於同一條鏈表中的所有節點的hash值相同):(2oldCap -1) & e.hash=(oldCap -1) & e.hash+oldCap.[注意:1.舊數組長度oldCap爲2的n次冪;2.計算某鏈表節點e在長度爲n的數組中的索引下標的公式爲(n-1)&e.hash;3.數據e的hash是根據e的key計算得到的,公式爲(h=key.hashCode()) ^ (h>>>16)].
      if (hiTail == null) //高位鏈表的末尾爲null,對於每個鏈表來說,說明是第一次走到這裏,而且此處也只會走進來一次,因爲後續會將非null的賦值給hiTail了。
      hiHead = e; //說明e爲高位鏈表頭節點,並將其賦給代表高位鏈表頭節點的hiTail
      else//說明高位鏈表末尾不爲null,說明至少處理過一次hiTail了,即頭節點肯定已經處理過了,下面應該去處理高位鏈表頭節點的後續節點了
      hiTail.next = e; //處理完高位鏈表頭節點後,根據next=e.next和while((e=next)!=null),依次循環遞進處理高位鏈表頭節點的後續節點,將舊數組中的鏈表頭節點的後續節點,追加到高位鏈表頭節點loHead的next裏。
      hiTail=e; //將非null的e值賦給hiTail,首次走到這裏時,hiTail和hiHead都指向e。
      }
      } while ((e = next) != null); //鏈表後續還有節點時,才繼續處理,否則跳出循環
      if (loTail != null) {//低位鏈表尾節點不爲空,說明舊數組向低位鏈表的數據轉移已處理完,可做進一步處理
      loTail.next = null; //要保證低位鏈表尾節點的後續節點爲null
      newTab[j] = loHead; //loHead代表了低位鏈表的頭節點,也就代表了整條低位鏈表(其上已經將舊數組中j索引位置上的鏈表裏的所有節點都轉移到了該低位鏈表上),而從前面的處理邏輯可知,低位鏈表移動到新數組時的索引下標位置,與在舊數組上的索引位置相同,故直接將低位鏈表頭節點賦給新數組的j索引下標位置即完成轉移。
      }
      if (hiTail != null) {//高位鏈表尾節點不爲空,說明舊數組向高位鏈表的數據轉移已處理完,可做進一步處理

               hiTail.next = null; //要保證高位鏈表尾節點的後續節點爲null 
               newTab[j + oldCap] = hiHead; // hiHead代表了高位鏈表的頭節點,也就代表了整條高位鏈表(其上已經要移動到新數組(j+oldCap)索引位置上的所有節點都轉移到了高位鏈表上),而從前面的處理邏輯可知,高位鏈表移動到新數組時的索引下標位置,與在舊數組上的索引位置相差了一箇舊數組長度oldCap,故直接將高位鏈表頭節點賦給新數組的(j+oldCap)索引下標位置即完成轉移。
             }
           }
         }
       }
      

      }
      return newTab; //返回處理完的新數組
      }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章