HashMap底層源碼剖析

  • 2020 年 3 月 17 日
  • 筆記

HashMap底層源碼剖析

一、HashMap底層用到的數據結構

數組+單向鏈表+紅黑樹

  數組:數組每一項都是一個鏈表,其實就是數組和鏈表的結合體

  單向鏈表:當法神hash碰撞時,首先會找到數組對應位置,然後1.8採用尾插入法(1.7採用頭插入法),形成一個單項鏈表結構

  JDK1.8 紅黑樹:當數組中每項的鏈表長度大於8時,會轉換為紅黑樹

二、什麼是hash碰撞?解決方案?

  hash碰撞:不同的key可能會產生相同的hash值;

  方案:鏈表發,再哈希法;

  hashMap中採用鏈表發,在ConcurrentHashMap中採用哈希法;

二、紅黑樹與二叉樹比較

  二叉查找樹在特殊情況下也會變成線性結構,和原來鏈表有共同的問題,節點太深,查找性能慢;

  紅黑樹相比二叉樹,在檢索的時候效率其實差不多,都是通過平衡來二分查找。但對於插入刪除等操效率提高很多。紅黑樹不像二叉樹一樣追求絕對的平衡,它允許局部很少的不完全平衡,這樣對於效率影響不大,但省去了很多沒有必要的調平衡操作,二叉樹調平衡有時候代價較大,所以二叉樹的效率不如紅黑樹;

三、為什麼採用紅黑樹

  在平常我們用HashMap的時候,HashMap裏面存儲的key是具有良好的hash算法的key(比如String、Integer等包裝類),衝突幾率自然微乎其微,此時鏈表幾乎不會轉化為紅黑樹,但是當key為我們自定義的對象時,我們可能採用了不好的hash算法,使HashMap中key的衝突率極高,但是這時HashMap為了保證高速的查找效率,就引入了紅黑樹來優化查詢了。

四、為什麼臨界值為8

  通過源碼我們得知HashMap源碼作者通過泊松分佈算出,當桶中結點個數為8時,出現的幾率是億分之6的,因此常見的情況是桶中個數小於8的情況,此時鏈表的查詢性能和紅黑樹相差不多,因為轉化為樹還需要時間和空間,所以此時沒有轉化成樹的必要。

  當數據較少的時候,採用鏈表要比紅黑樹效率高,因為平衡二叉樹保持平衡需要耗費資源,那麼前期數據較少時採用鏈表,當鏈表中的數據長度大於8時,就將鏈錶轉換成紅黑樹,可以加快數據的插敘速度,官方測試8為性能最優

五、put()底層分析

    public V put(K key, V value) {          return putVal(hash(key), key, value, false, true);      }        /**       * Implements Map.put and related methods       *       * @param hash hash for key       * @param key the key       * @param value the value to put       * @param onlyIfAbsent if true, don't change existing value       * @param evict if false, the table is in creation mode.       * @return previous value, or null if none       */      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                     boolean evict) {          Node<K,V>[] tab; Node<K,V> p; int n, i;          //判斷當前數組是否為空,如果為空要進行第一次擴容          if ((tab = table) == null || (n = tab.length) == 0)              //擴容後將擴容大小交給N              n = (tab = resize()).length;          //判斷獲取當前數組位置是否存在數據,如果為空則直接插入,否則需要代表當前位置不是空的,不是空的需要判斷          if ((p = tab[i = (n - 1) & hash]) == null)              //如果為空則創建一個新的節點添加到該位置              tab[i] = newNode(hash, key, value, null);          else {              Node<K,V> e; K k;              //判斷Hash值和Key值是否相同,如果相同則需要Value覆蓋              if (p.hash == hash &&                  ((k = p.key) == key || (key != null && key.equals(k))))                  e = p;              //判斷當前數組中存放的節點是否是樹節點,則添加樹節點即可              else if (p instanceof TreeNode)                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);              else {                  //循環遍歷鏈表                  for (int binCount = 0; ; ++binCount) {                      //判斷當前數組該位置的值得下一個元素是否為空,如果為空則追加到當前元素後邊                      if ((e = p.next) == null) {                          p.next = newNode(hash, key, value, null);                          //添加完畢後判斷當前鏈表節點有多少個,如果節點大於等於8則轉換為紅黑樹                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                              //treeifyBin判斷當前數組是否為空,或者長度是否小於64,如果為空或者小於64,則先擴容                              treeifyBin(tab, hash);                          break;                      }                      //再次進行Key的重複判斷                      if (e.hash == hash &&                          ((k = e.key) == key || (key != null && key.equals(k))))                          break;                      p = e;                  }              }              //表明,記錄到具有相同元素的節點              if (e != null) { // existing mapping for key                  V oldValue = e.value;                  if (!onlyIfAbsent || oldValue == null)                      e.value = value;                  //這個是空函數,可以有用戶根據需要覆蓋                  afterNodeAccess(e);                  return oldValue;              }          }          ++modCount;          //判斷當前數組元素的個數和閾值進行比較,如果數量大於閾值則需要擴容          if (++size > threshold)              //默認情況下,第一次添加數據的時候,先會進行一次擴容後再添加數據,後續都是先添加數據在進行擴容              resize();          //這個是空函數,可以有用戶根據需要覆蓋          afterNodeInsertion(evict);          return null;      }          

在上述的方法中,設計三種情況:

  第一種情況,數組索引位置沒有鍵值對,處理方式就是直接把待添加鍵值對封裝成Node添加到索引位置即可;

  第二種情況,如果數組索引位置有鍵值對,而且封裝的TreeNode節點,處理方式是調用紅黑樹的插入方法,把帶添加鍵值對添加到紅黑樹中;

  第三種情況,同樣數組索引位置有鍵值對,但是封裝的是Node節點,處理方法就比較複雜,首先把待添加鍵值對封裝成Node節點添加到鏈表尾部,然後判斷當前鏈表長度,如果達到閾值,就判斷是擴容還是轉換為紅黑樹;

  

六、get()底層分析

    public V get(Object key) {          Node<K,V> e;          return (e = getNode(hash(key), key)) == null ? null : e.value;      }        /**       * Implements Map.get and related methods       *       * @param hash hash for key       * @param key the key       * @return the node, or null if none       */      final Node<K,V> getNode(int hash, Object key) {          Node<K,V>[] tab; Node<K,V> first, e; int n; K k;          //判斷數組以及數組對應位置數組元素是否為空          if ((tab = table) != null && (n = tab.length) > 0 &&              (first = tab[(n - 1) & hash]) != null) {              //用get傳遞過來的Key值和對應位置第一個元素進行比較,如果相等直接返回,如果不等則進行查找              if (first.hash == hash && // always check first node                  ((k = first.key) == key || (key != null && key.equals(k))))                  return first;              //判斷第一個元素的下一個元素是否為空              if ((e = first.next) != null) {                  //判斷當前節點是否為樹節點                  if (first instanceof TreeNode)                      //如果是樹節點,直接通過getTreeNode拿到該節點返回                      return ((TreeNode<K,V>)first).getTreeNode(hash, key);                  //循環一一對比                  do {                      if (e.hash == hash &&                          ((k = e.key) == key || (key != null && key.equals(k))))                          return e;                  } while ((e = e.next) != null);              }          }          return null;      }

七、擴容機制底層分析

  在HashMap中,桶數組的長度均是2的冪,閾值大小為桶數組長度與負載因子的乘積。當HashMap中的鍵值對數量超過閾值時,就進行擴容;

  擴容之後,要重新計算鍵值對的位置,並把它們移到合適的位置上去;

    /**       * Initializes or doubles table size.  If null, allocates in       * accord with initial capacity target held in field 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.       *       * @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;          //如果table不為空,表明已經初始化過了          if (oldCap > 0) {              //當table容量超過容量最大值,則不再擴容              if (oldCap >= MAXIMUM_CAPACITY) {                  threshold = Integer.MAX_VALUE;                  return oldTab;              }              //按舊容量或閾值的2倍計算新容量和閾值大小              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              //初始化時,將threshold的值賦值給newCap;           //HashMap使用threshold變量暫時保存initialCapacity參數的值              newCap = oldThr;          else {               // zero initial threshold signifies using defaults              //調用無參構造方法時,桶數組容量為默認容量;閾值為默認容量與默認負載因子              newCap = DEFAULT_INITIAL_CAPACITY;              newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);          }          //newThr為0時,按閾值計算公式進行計算          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;      }      

擴容總共做了三件事:

  1.計算新桶數組的容量newCap和新閾值newThr

  2.根據計算出的newCap創建新的桶數組,桶數組table也是這裡進行初始化的

  3.將鍵值對節點重新映射到新桶數組中,如果節點是TreeNode類型,則需要拆分紅黑樹;如果是普通節點,則節點按原順序進行分組