HashMap的擴容

HashMap默認數組大小是16,研究表明,當數組長度爲2的n次冪的時候,不同的key算得的index相同的機率較小,數據在數組上分佈的比較均勻,也就是產生hash碰撞的機率比較小,相對的,數據存放在鏈表上的機率比較小,查詢效率也就比較高了。
所以在存儲大數量數據的時候,最好指定hashmap的size爲2的整數次冪,就算不指定的話,也會以大於且接近於指定值大小的2次冪來初始化的。預先指定的話,可以減少resize次數,提高效率。

hashmap在什麼時候會進行擴容呢?
如果沒有預先指定hashmap的大小的話,初始化的時候會默認數組大小爲16,負載銀子爲0.75,當hashmap中存放的數據個數超過數組大小*loadFactor時,就會進行數組擴容,擴大一倍;例如當map中數據數量大於16*0.75=12的時候,就會把數組大小擴展爲2*16=32個,然後重新計算每個元素在數組中的位置,重新散列存放。這是一個非常消耗性能的過程,所以如果我們能預知hashmap中元素的個數,預置好可以提高hashmap的性能。
比如我們知道元素個數1000,那麼應該預置爲:length*0.75>1000;而且保證length是2的n次冪,所以length應該設置爲2048,new HashMap(2048)。

key如果是自定義對象,那麼一定要重寫equals和hashcode方法。

什麼時候擴容:當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值---即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。

擴容(resize)就是重新計算容量,向HashMap對象裏不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然Java裏的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

擴容源碼:

JDK1.7

void resize(int newCapacity) {   //傳入新的容量  
    Entry[] oldTable = table;    //保存舊的entry數組,引用擴容前的Entry數組  
    int oldCapacity = oldTable.length;//保存舊的容量  
    if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的數組大小如果已經達到最大(2^30)了  
        threshold = Integer.MAX_VALUE; //修改閾值爲int的最大值(2^31-1),這樣以後就不會擴容了(容量達到了最大值)  
        return;  
    }  
  
    Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry數組  
    transfer(newTable);                         //!!將數據轉移到新的Entry數組裏  
    table = newTable;                           //HashMap的table屬性引用新的Entry數組  
    threshold = (int) (newCapacity * loadFactor);//修改閾值 -- 產生新的閾值 
}  

    /**
     * Transfers all entries from current table to newTable.
     */
   void transfer(Entry[] newTable) {  
       Entry[] src = table;             //src引用了舊的Entry數組  
       int newCapacity = newTable.length;//計算新的數組容量  
       for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組  
         Entry<K, V> e = src[j];             //取得舊Entry數組的每個元素  
         if (e != null) {  
         src[j] = null;//釋放舊Entry數組的對象引用(for循環後,舊的Entry數組不再引用任何對象)  
             do {  
                  Entry<K, V> next = e.next;//每個entry都包含一個next指針, 頭插式 
                  int i = indexFor(e.hash, newCapacity); //!重新計算每個元素在數組中的位置  
                  e.next = newTable[i]; //標記[1]  
                  newTable[i] = e;      //將元素放在數組上  
                  e = next;             //訪問下一個Entry鏈上的元素  
            } while (e != null);  
        }  
    }  
}  

 static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

下面我們講解下JDK1.8做了哪些優化。經過觀測可以發現,我們使用的是2次冪的擴展(指長度擴爲原來2倍),所以,

經過rehash之後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。對應的就是下方的resize的註釋。

元素在重新計算hash之後,因爲n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化

因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖爲16擴充爲32的resize示意圖

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認爲是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊

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