原始碼-JDK1.8HashMap擴容方法resize()解析

醬油瓶啤酒杯發表於2020-12-13

1、準備知識

  • HashMap的底層資料結構

    Java語言的基本資料結構可以分為兩種,一種是陣列,另一種的模擬指標/引用,Java語言中涉及到的資料結構都是這兩種的擴充。JDK1.8之前HashMap是陣列+連結串列結合的連結串列雜湊。JDK1.8在解決雜湊衝突上發生了變化,當連結串列長度大於閾值/預設8的時候,會將連結串列轉化為紅黑樹,減少搜尋時間。

在這裡插入圖片描述

  • hash演算法

    我們希望HashMap的元素位置儘量分散,最好是每個位置只有一個元素,這樣用hash演算法求得該位置後可以直接返回結果,不用再遍歷連結串列/紅黑樹。

    HashMap通過Key的hashCode經過擾動函式(就是hash方法)處理後得到hash值,然後在公式(n-1)& hash (/n是陣列長度)判斷當前元素的位置是否已存在元素,若已存在則判斷新加入的元素和已存在的是否相同,相同則覆蓋,不相同則拉鍊法解決衝突。

    //擾動函式 == hash方法
    /*JDK1.7
    此函式可確保僅在以下方面不同的hashCode每個位位置的恆定倍數有界碰撞次數(預設負荷係數下約為8)。
    */
    static int hash(int h){
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
     
    /*JDK1.8的hash方法比JDK1.7的更加簡化,JDK1.7的擾動需要4次,JDK1.8的僅需1次*/
    static int hash(Object key){
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    • 拉鍊法:將連結串列和陣列相結合。也就是說建立⼀個連結串列陣列,陣列中每⼀格就是⼀個連結串列。若遇到雜湊衝突,則將衝突的值加到連結串列中即可。下次查詢的時候,需要遍歷這個連結串列找到元素,減慢了查詢效率。

    • ->所有,在儲存大規模的資料時,預先指定HashMap的大小為2的冪次方

      int capacity = 1;
      while(capacity < initialCapacity){
          capacity <<= 1;//擴大兩倍
      }
      

    2、HashMap的resize()方法介紹

    • 當HashMap中的元素增多時,發生雜湊衝突的機率就越來越高,這時候為了提高查詢的效率,需要對陣列大小進行擴容。

    • 當陣列的元素規模 > 陣列大小 * loadFactory時進行擴容,loadFactory預設是0.75

      ... map = new HashMap<>(c, loadFactor)//HashMap提供了以上有參構造方法
          //initialCapacity是HashMap陣列的初始容量
          //loadFactor是載入因子
      
    • 什麼時候進行resize?1、初始化table的時候 2、陣列元素size超出threshold = map.size() * loadFactory

      直接擴容 原陣列的2倍大小

    • 節點在轉移的過程中是一個個節點複製還是一串一串的轉移?從原始碼中我們可以看出,擴容時是先找到拆分後處於同一個桶的節點,將這些節點連線好,然後把頭節點存入桶中即可

    • 話不多說,上原始碼

    • final Node<K,V>[] resize() {
              Node<K,V>[] oldTab = table;
              int oldCap = (oldTab == null) ? 0 : oldTab.length;
              int oldThr = threshold;
              int newCap, newThr = 0;
              //如果原table不為空
              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
              }
              /**
              * 從構造方法我們可以知道
              * 如果沒有指定initialCapacity, 則不會給threshold賦值, 該值被初始化為0
          	* 如果指定了initialCapacity, 該值被初始化成大於initialCapacity的最小的2的次冪
      		* 這裡這種情況指的是原table為空,並且在初始化的時候指定了容量,
      		* 則用threshold作為table的實際大小
      		*/
              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);
              }
              // 計算指定了initialCapacity情況下的新的 threshold
              if (newThr == 0) {
                  float ft = (float)newCap * loadFactor;
                  newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                            (int)ft : Integer.MAX_VALUE);
              }
              threshold = newThr;
      
      
          /**從以上操作我們知道, 初始化HashMap時, 
          *  如果建構函式沒有指定initialCapacity, 則table大小為16
          *  如果建構函式指定了initialCapacity, 則table大小為threshold,
          *  即大於指定initialCapacity的最小的2的整數次冪
          
          *  從下面開始, 初始化table或者擴容, 實際上都是通過新建一個table來完成
          */ 
      
              @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) {
                        /** 這裡注意, table中存放的只是Node的引用,這裡將oldTab[j]=null只是清除舊錶的引用, 
                         * 但是真正的node節點還在, 只是現在由e指向它
                         */
                          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()操作之連結串列拆分

        /**
        這裡定義了四個節點:loHead, loTail ,hiHead , hiTail,兩個頭節點兩個尾節點
        選擇出擴容後在同一個桶中的節點,直接將節點連線好頭節點入桶即可。
        **/
            HashMap.Node<K,V> loHead = null, loTail = null;
            HashMap.Node<K,V> hiHead = null, hiTail = null;
            HashMap.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;
            }
        

        img

相關文章