經常使用HashMap你真的瞭解它麼

HashMap的存儲結構

說到這裏大家都能脫口而出存儲的是鍵值對,可是鍵值對是怎麼存儲的很多人都不太瞭解。
我們需要對HashMap內部三種數據結構進行了解。

  1. 普通節點(鏈表)
  2. 紅黑樹節點
  3. 節點數組(桶)

普通節點

HashMap中維護的這個內部類實現了Entry接口,其實就是我們所熟悉的鍵值對,並且由於該類裏面有指向下一個節點的引用,所以可以把這個節點理解爲一個鏈表

// 這個節點就是我們使用的鍵值對
static class Node<K,V> implements Map.Entry<K,V> {
        // 鍵值對key值的hash(這裏面的hash是經過處理過的)
        final int hash;
        // 鍵值對的key值
        final K key;
        // 鍵值對的value值
        V value;
        // 下一個節點
        Node<K,V> next;
}

紅黑樹節點

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        // 父節點
        TreeNode<K,V> parent;  // red-black tree links
        // 左孩子
        TreeNode<K,V> left;
        // 又孩子
        TreeNode<K,V> right;
        // 刪除時取消連接用的
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        // 標識是紅節點還是黑節點
        boolean red;
}

節點數組

該數組裏面存放的是普通節點的頭結點,或者紅黑樹的根節點。

因爲TreeNode繼承於LinkedHashMap.Entry,而LinkedHashMap.Entry繼承於HashMap.Node

總結

  1. HashMap內部維護了一個桶,桶內存放的是若干鏈表及紅黑樹。

HashMap的存操作

該操作可以分爲下面幾個步驟:

  1. 計算key值hash,判斷應該存放在桶內哪個位置
  2. 將該鍵值對放在對應位置鏈表的末尾
  3. 判斷該鏈表長度大於等於8時,將該鏈表轉化爲紅黑樹
  4. 判斷桶內鏈表及紅黑樹的個數和是否大於當前桶的大小*加載因子(默認0.75)
  5. 大於則rehash擴容桶的大小

下面我們針對這幾個步驟瞭解一下里面的處理邏輯

計算key值hash

下面是計算hash的源碼

    static final int hash(Object key) {
        int h;
        // 從這裏可以說明hashmap的key值可以爲null,爲null的hash默認爲0
        // hashtable取hash是用的下面兩句話,這也解釋了爲什麼hashtablekey值不能爲空
        // Entry<?,?> tab[] = table;
        // int hash = key.hashCode();
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

可以分爲下面幾個步驟

  1. 計算key的hash值
  2. 將hash值向右無符號位移16位
  3. 原hash與右移過的hash位異或運算

第一步到第三步稱爲hashmap中的擾動函數,爲了就是減少hash碰撞

爲什麼要右移16位

結論:因爲右移16位可以讓高位參與運算。
原理:

  1. hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。
  2. int類型爲2進制32位的,右移16位看好是的前16位高半區設0,高半區設到後16位低半區

爲什麼要原hash與右移過的hash位異或運算

結論:使得低半區同時具有高半區和低半區的特性。
原理:

  1. 位移過後低半區的值不在保留,所有隻有高半區的特性。
  2. 只有經過異或運算纔可以加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。

判斷應該存放在桶內哪個位置

HashMap是通過將經過擾動算法計算出的hash值與當前桶的大小減一再進行與運算得出來的一個數組下標

爲什麼要與運算

其實這個沒啥原理啥的。。。就因爲內存不夠大,上面也提到過hashcode是個表值範圍是從-2147483648到2147483648。前後加起來大概40億的映射空間如果你內存足夠大能發下40億長度的數組,鬼才做擾動。所以只能儘可能的減少碰撞,出現碰撞再轉化爲鏈表或者紅黑樹。
順便說一下,這也正好解釋了爲什麼HashMap的數組長度要取2的整次冪。因爲這樣(數組長度-1)正好相當於一個“低位掩碼”。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。

將該鍵值對放在對應位置鏈表的末尾

在1.8之前都是從頭部開始插入的,但是從頭部開始插入,會使得擴容時該錶鏈表的位置在併發情況下會形成環,所以從1.8之後改成了從尾部開始。

判斷該鏈表長度大於等於8時,將該鏈表轉化爲紅黑樹

因爲鏈表如果太長的話會會降低查詢效率,因爲鏈表平均查找長度爲n/2。
紅黑樹平均查找長度爲logn。
所以如果鏈表長度過長的話爲了增加查詢效率則需要轉化爲紅黑樹。而選擇8這個閾值是因爲
log8 = 3。 8/2 =4。超過8紅黑樹的查詢效率就要高於數組。
並且紅黑樹還需要左旋右旋,所以節點的少的時候鏈表更優。

判斷是否需要擴容

當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對hashmap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認情況下,數組大小爲16,那麼當hashmap中元素個數超過160.75=12的時候,就把數組的大小擴展爲216=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的性能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設置爲1024。 但是new HashMap(1024)還不是更合適的,因爲0.751000 < 1000, 也就是說爲了讓0.75 * size>1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章