深度解析HashMap

Onvertex發表於2021-12-22

講講HashMap?

原始碼解析

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
  	//輔助變數
        Node<K,V>[] tab; Node<K,V> p; int n, i;
 	//如果當前tabe陣列是null,數量是0的話
        if ((tab = table) == null || (n = tab.length) == 0)
            //執行擴容方法resize()
       	    //初始化陣列預設大小是16
            n = (tab = resize()).length;
  	//通過hash與運算獲得陣列索引位置,該位置是null
        if ((p = tab[i = (n - 1) & hash]) == null)
            //建立新Node物件
            tab[i] = newNode(hash, key, value, null);
        else {//該索引位置不為null
            Node<K,V> e; K k;
          	//當前table索引hash和新的索引hash相同 && 
               //(當前table節點的key和新key是同一物件 || equals為真)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          	//如果當前table的node已是紅黑樹就按紅黑樹處理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //如果節點是後面是連結串列就遍歷比較
                for (int binCount = 0; ; ++binCount) {
                    //遍歷整個連結串列結束,也沒有和新的key相同的就新增連結串列後面新建立node
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                       //加入後判斷當前的連結串列個數是否已經達到8個,如果達到就進行紅黑樹轉換
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          //提示:
                          //treeifyBin裡如果table為null或者大小小於64,暫時不會轉化為紅黑樹
                          //而是進行擴容。
                          //if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                            treeifyBin(tab, hash);
                        break;
                    }
                   //發現相同就break結束遍歷
                    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;
        //每增加node,Size++
  	//如果數量大於臨界值,就進行擴容
  	//threshold=table大小(預設16)*負載因子(預設0.75)
  	//12>24>>48
        if (++size > threshold)
          	//擴容成2倍增長16->32->64
            resize();
        afterNodeInsertion(evict);
        return null;
    }

簡述

Jdk1.7 陣列+連結串列

Jdk1.8 陣列+連結串列+紅黑樹

hashMap 預設陣列大小16,負載因子 預設0.75, 臨界值= 陣列大小*負載因子。

  1. 首先將key進行hash演算法,key的hashCode右移16位並進行異或運算。
  2. 如果table是null ,先初始化陣列預設大小是16
  3. 通過hash的與運算獲得當前table索引位置,如果索引位置內容是null則建立新Node節點物件
  4. 如果當前table索引位置裡內容不為null,則尋找相同key直接修改值,分三種情況
    1. 當前table索引的物件hash是否與新hash相同並且當前table位置物件key是否和新key相同
    2. 如果當前table索引的物件已是TreeNode(紅黑樹)就進行遍歷樹方式查詢
    3. 如果是連結串列則遍歷連結串列查詢,如果遍歷完也沒找到就往連結串列尾部加入並建立新Node,在判斷當前大小是否大於等於8,如果大於就進行紅黑樹轉化。執行紅黑樹轉化方法時有個條件,如果陣列大小小於64則還是進行擴容操作。
  5. 當前Size陣列大小和threshold(臨界值)比較,如果size大於threshold則進行擴容(2倍增長),

問題

JDK 1.8中對hash演算法和定址演算法是如何優化的?

//hash演算法優化
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//(n - 1) & hash 定址演算法優化   
if ((p = tab[i = (n - 1) & hash]) == null)

配合來說,put鍵值時,肯定涉及陣列長度的取模運算,但是在計算機上面(n - 1) & hash位運算效率高於普通的除法取餘。關鍵這個n(hashmap長度)通常會很小,但是hash值是32位的,因此(n-1)大概率高位補零,由於與運算是隻有兩個數字都是1結果才為1,其他情況均為0,因此hash值的高16位永遠起不到作用,這樣就大大的增加了定址衝突概率,除非陣列長度很大,撐滿整個32位最好都為1,才不會有衝突,但一般不可能很長,所以通過hash演算法的優化(h = key.hashCode()) ^ (h >>> 16) 這個公式完美的讓hash值高16位與低16位融合,保留高低16位的特徵。再跟(n-1)做與運算,hash衝突的概率就降低了!

相關文章