溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

HashMap容量和負載因子使用說明

發布時間:2020-10-02 01:57:05 來源:腳本之家 閱讀:272 作者:ye17186 欄目:開發技術

HashMap底層數據結構是數組+鏈表,JDK1.8中還引入了紅黑樹,當鏈表長度超過8個時,會將鏈表轉成紅黑樹,以提升其查找性能。

那么,給出一個<key, value>節點,HashMap是如何確定這個節點應該放在具體哪個位置呢?(以JDK1.8為例)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // HashMap沒有被初始化,則先進行初始化
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // 節點所在index = (n - 1) & hash,該位置沒有數據,則直接將新節點放在數組的index位置上
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else { // index上已經有節點了
    Node<K,V> e; K k;
    // 如果新key與原來的key一樣,則e指向原節點p(后面會用新value替換e所指向的value)
    if (p.hash == hash &&
      ((k = p.key) == key || (key != null && key.equals(k)))) 
      e = p;
    // 如果該節點是樹節點,則采用樹的插入算法,插入新節點
    else if (p instanceof HashMap.TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else { // 該節點是鏈表節點
      for (int binCount = 0; ; ++binCount) {
        // 將新節點插入到index所在鏈表的末端
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          // 鏈表節點超過8個,則進行鏈表轉樹處理
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        // 同樣的,如果key已經存在的話,則不進行插入操作,而是后面進行value替換
        if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    // e != null的情況,就是key已經存在了,這里統一進行了新值value,替換舊值e.value的操作
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  // 插入后數組size 大于閾值的話,需要進行擴容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

看源碼,節點落在數組中的index = (數組長度 - 1) & key的hashcode,如果該index上沒有數據,則直接插到該index上,如果節點已經有數據了,則把新節點插入該index對應的鏈表中(如果鏈表節點大于8個,會進行鏈表轉樹,之后的插入算法就變成了樹的插入算法)。

每次put之后,會檢測一下是否需要擴容,size超過了 總容量 * 負載因子,則會擴容。默認情況下,16 * 0.75 = 12個。

HashMap容量和負載因子使用說明

1、為什么初始容量是16

當容量為2的冪時,上述n -1 對應的二進制數全為1,這樣才能保證它和key的hashcode做&運算后,能夠均勻分布,這樣才能減少hash碰撞的次數。至于默認值為什么是16,而不是2 、4、8,或者32、64、1024等,我想應該就是個折中處理,過小會導致放不下幾個元素,就要進行擴容了,而擴容是一個很消耗性能的操作。取值過大的話,無疑會浪費更多的內存空間。因此在日常開發中,如果可以預估HashMap會存入節點的數量,則應該在初始化時,指定其容量。

2、為什么負載因子是0.75

也是一個綜合考慮,如果設置過小,HashMap每put少量的數據,都要進行一次擴容,而擴容操作會消耗大量的性能。如果設置過大的話,如果設成1,容量還是16,假設現在數組上已經占用的15個,再要put數據進來,計算數組index時,發生hash碰撞的概率將達到15/16,這違背的HashMap減少hash碰撞的原則。

補充知識:HashMap只有容量達到閥值才發生擴容嗎?大錯特錯!

看了網上很多文章,說HashMap在元素達到負載因子對應數的時候就發生擴容。如果你看過源碼就會發現,其實還有一種情況也可能會發生擴容:樹形化的時候。

對象最終是如何放入HashMap中的?

HashMap底層是由數組+鏈表組成的,為了方便不懂的人更容易理解,那我們就先假設HashMap底層就是數組,先不管鏈表。

當一個對象add到HashMap中,此時HashMap的add方法是如何來確定這個對象是放在數組中的哪個位置的呢?

拿JDK1.8來說(其他JDK版本稍有不同,但大同小異),大家應該知道每一個對象天生都繼承了或程序員自己覆蓋了Object類的 hashCode()方法,此方法返回對象的hashcode值。

HashMap會有一個方法,先拿到要add進HashMap中的對象的hashCode,再將這個hashCode異或上對象自身hashCode右移16位(是不是感覺說的不是人話?這個步驟叫擾亂,這樣做的目的是為了讓hashCode每一位都盡可能用到,如果不理解沒關系并不影響接下來的閱讀),hashCode經過上述步驟之后再&(數組長度-1),計算的結果就是這個對象在數組中的位置了。我自己都覺得說的不是人話,下面舉個例子,便于理解:

這里有一個Student對象的hashCode是:a

先把這個a右移16位 , b=a>>>16;

然后a=a&b;

數組中的位置等于: a&(數組長度-1);

上述源碼如下:

h=key.hashCode();

h = key.hashCode()) ^ (h >>> 16)

數組位置=h&(數組長度-1);

好了, 我們已經知道元素是如何在hashMap中的數組上如何定位了,現在假設一個極端情況(不可能發生,但是我用這個舉例子):

假設數組長度為1,根據源碼:

數組位置=h&(數組長度-1)

那么有:

數組位置=h&(1-1)=0 ,無論什么對象,都定位到數組的第0個位置。

這個很好理解吧。無論元素是否一樣,由于數組長度為1,所以元素通通定位到數組中第0個位置。大家都知道一個數組只能放一個元素???那怎么辦呢?我們用鏈表來解決這個問題,把定位到這個位置的元素通過鏈表連接。這就是我一開始說的:hashMap是數組+鏈表。

那樹形化又是什么東東呢?

想一下我們為什么要用HashMap,是因為通過Hash算法在理想情況下時間復雜度O(1)就能找到元素,特別快,但是我都說了是理想情況,如果遇到上述發生hash碰撞(誰jb取的名字,就是上面我才說的,兩個元素定位到數組中同一個位置),且hash碰撞比較頻繁的話,那么當我們get一個元素的時候,定位到了這個數組,還需要在數組中遍歷一次鏈表最終才能找到要get的元素,是不是已經失去一部分使用HashMap的初心了?(因為需要遍歷鏈表,所以時間復雜度就比之前高了)

所以JDK1.8使用紅黑樹這種數據結構來解決鏈表過長的問題(可以簡單理解為用紅黑樹遍歷比鏈表遍歷速度快,時間復雜度低,不懂紅黑樹的可以去搜搜看),默認鏈表長度達到8就將鏈表樹形化(變為紅黑樹)。

回到最最開始我提到的,那為什么樹形化的時候可能會發生擴容呢?

想想剛剛的例子數組長度為1,所有元素全部在數組的第0個位置形成一條鏈表,這例子是一種極端情況,數組長度過小,那自然就會經常發生hash碰撞,那形成長鏈表是肯定的,這個時候樹形化其實是治標不治本,因為引起鏈表過長的根本原因是數組過短,所以在JDK1.8源碼中,執行樹形化之前,會先檢查數組長度,如果長度小于64,則對數組進行擴容,而不是進行樹形化。

所以發生擴容的時候有兩種情況,一種是元素達到閥值了,一種是HashMap準備樹形化但又發現數組太短,這兩種情況均可能發生擴容。

以上這篇HashMap容量和負載因子使用說明就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持億速云。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女