1. 程式人生 > >[HashMap原始碼學習之路]---陣列擴容後元素的前後變化

[HashMap原始碼學習之路]---陣列擴容後元素的前後變化

HashMap陣列擴容後元素的前後變化

  前一段時間看了HashMap 的擴容方法,覺得寫的太好了,對我很有幫助,現以我理解的來寫一下。主要說兩方面:

  1. 擴容後元素的位置
  2. 擴容後元素如何分佈的

1、resize方法的原始碼

  HashMap 中擴容方法為resize() 。程式碼如下:

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) { 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 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; @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) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; 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 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; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

  上邊是整個resize() 方法的原始碼,今天說的是其中的這部分,即從上邊帶有註釋的說明① 位置下邊的if (oldTab != null) { 這一行開始。
  從這一行的判斷裡看到,意思是原有table 裡如果沒有元素的話,會走的邏輯,其實就是hashMap 針對於陣列長度達到負載因子*陣列長度 的時候,會進行資料的重新分佈,這一步上也是效能消耗的地方。

2、擴容後陣列元素的位置(無鏈)

  先來看上邊原始碼的說明② 位置,即下邊這行:

if (e.next == null)
   newTab[e.hash & (newCap - 1)] = e;

  這行的意思是,如果當前陣列下標上有元素,但是沒有子元素,也就是沒有形成鏈,就只有一個元素 ,那麼該元素放置的位置按照newTab[e.hash & (newCap - 1)] 來放置。
  首先來看一張圖,hashMap 中,確定元素在陣列哪個位置,是通過hash 值(通過一個hash方法進一步運算過的)與陣列長度減一進行與運算 得到的,如下圖所示:
這裡寫圖片描述
  做法特別巧妙,那如果擴容後,比如對上邊的陣列長度16 ,現在變成了長度為32 ,也就是上邊提到的這個newTab[e.hash & (newCap - 1)] ,相同的三個數,在擴容後的放置位置是什麼呢?看下圖:
這裡寫圖片描述
  為什麼會出現不同的位置呢,請仔細看那兩個hash數,對應的100000 中的1 上的數,有的有1 ,有的有0 ,因為這是個隨機數,並不知道到底是幾,反正不是0 ,就是1 了,這樣就會使算出來的下標,分到兩個位置上,使擴容後的陣列,對元素又均勻的分攤一下。

  總結:HashMap 擴容後,原來的元素,要麼在原位置,要麼在原位置+原陣列長度 那個位置上。

這裡寫圖片描述

2、擴容後元素的位置(有鏈)

  借用別人的圖,如下:
這裡寫圖片描述
  通過上圖來看,左邊是擴容前 ,右邊是擴容後 ,對於下標為15 上整條鏈上的元素,擴容一倍後,元素要麼在原位置15 上,要麼在原位置加原陣列長度,即15+16 那個位置上。並且整條鏈上的元素,不管在原位置15,還是在31 上,它們的排列順序沒有變化。(據說這與1.7版本不同,我沒看過1.7版本)
  那是如何做到的呢,再繼續看上邊的原始碼,位置在上邊原始碼的說明③ 處,即下邊這個位置:

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;
      newTab[j + oldCap] = hiHead;
  }

  上邊的程式碼中,根據說明③ 處的(e.hash & oldCap) == 0 來將鏈上的元素分成兩份,然後又在說明④ 處,對元素分別放到了原位置原位置+原陣列長度 上。
  用幾個資料來舉個例子,就會如下圖所示:
  比如一個鏈上的資料如下圖,三種顏色,形成了鏈。
這裡寫圖片描述

  如果對於它們三個,擴容一倍後,就會變成下邊這樣:
這裡寫圖片描述
  理解的不對的話,請大神及時指正。
這裡寫圖片描述