HashMap原理基礎

數據結構分析

數據結構:數組+鏈表(或紅黑樹)

數組:Entry<K,V> implements Map.Entry<K,V>實力數組

鏈表:Entry內的next指向Entry實現

value是按數組存放的,int hash = hash(key.hashCode()); int i = indexFor(hash, table.length) 將value放到table中i位置,這樣能快速找到數據存儲位置,效率高。但是又涉及到數組中同一index存放不同的數據的情況

  • 關於鏈表解決衝突:

JAVA爲什麼兩個不同對象的hashCode有可能相同

public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                return oldValue;  
            }  
        }  
        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }  

從原碼分析:

map存數據的時候,是按table存儲的,如果相同index下的entry的hash和key都一樣,覆蓋就行了。但是不同的key值可能hash值相同,但是也要存儲到對應的index下。這就是hash衝突的根本原因,不同key的map要存儲到同一位置。那table的單個bucket中想要存放多個entry,顯然不對,所以HashMap底層採用鏈表解決衝突

數組中存放的是Entry實體鏈表,如果產生hash衝突,就插入到鏈表中。根本解決了hash列表中能一個bucket存放多個entry的問題

也可以從另一方面分析:table定長後,就算不同的hash值,indexFor得到的index也可能相同。鏈表解決的就是這個問題。來到同一index的entry,key值一樣的,hash必然也一樣,也就是覆蓋就行了。處理的就是那種不同key得到了相同的hashcode來到了同一index的entry,就要放到鏈表中。

總的理解hash衝突就是,不同key的數據要存到table的同一位置,核心問題就是不同keyhash值可能一樣。

  • 關於indexFor:

一般對哈希表求散列我們會使用hash值對length取模,HashTable就是這樣實現的,這種方法可以保證元素在哈希表中散列均勻,但取模會用到除法運算,效率非常低,HashMap是對HashTable進行改進後的實現,爲了提高效率,使用h&(length-1)取代除法運算,在保證散列均勻的基礎上還保持了效率的提升

具體算法:hash & (length-1),hash與上數組長度-1,等同於hash mod length

例:15%4=3,代替爲 15 & 3=1111 & 0011=0011=3

HashMap的性能問題

知道了HashMap的存儲結構,table+鏈表,比如存儲相同的元素個數,table小了,hash衝突就多了,鏈表就長了。相反,table大了,hash衝突就少了,鏈表就短了。

負載因子(load factor):

當創建 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子可以減少 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);減小負載因子會提高數據查詢的性能,但會增加 Hash 表所佔用的內存空間。負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。

HashMap構造:

默認初始的加載容量是16,加載因子是0.75。

//hashmap實際允許的最大元素個數
threshold = (int)(capacity * loadFactor);

當容量超出此最大容量時, resize後的HashMap容量擴大2倍。

void addEntry(int hash, K key, V value, int bucketIndex) {  
    Entry<K,V> e = table[bucketIndex];  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    if (size++ >= threshold)  
        resize(2 * table.length);  
        
        ...
}
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);//用來將原先table的元素全部移到newTable裏面
    table = newTable;  //再將newTable賦值給table
    threshold = (int)(newCapacity * loadFactor);//重新計算臨界值
}

一旦涉及到擴容,是比較耗費性能的,涉及到兩個列表數據的臨時交換,所以,在實際開發中,根據實際map的數據量,初始化HashMap的時候指定初始容量和負載因子。HashMap有幾種入參的構造方法,可以自行查看,一般開發中map數據也不會很大,指定初始化容量即可

注意:指定初始化容量,儘量是2的N次冪的數字,以降低重複率,不深入,可以看下這篇瞭解一下

成員變量 DEFAULT_INITIAL_CAPACITY 爲什麼是2的n次方

轉紅黑樹

參考文章(原文:https://blog.csdn.net/xiewenfeng520/article/details/107119970

紅黑樹查詢時間複雜度是O(logn),鏈表查詢查詢時間複雜度是O(n)。我們知道,put數據的key值是可以自己實現hashcode算法的,如果算法不好導致hash散列不那麼均勻,可能會導致table中某bucket的鏈表很長的,所以,就導致了查詢效率很低了。另外,雖然紅黑樹的查詢效率高,但是採用treenode的數據結構佔用的空間是鏈表的兩倍。所以,性能和空間的折中,得到一個閾值,當鏈表長度達到8時,轉爲紅黑樹結構。

閾值爲什麼是8

原碼給的有解釋:具體一坨英文註釋就不沾了,原碼給了鏈表長度達到8的命中概率計算如下

//註釋就不沾了

0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006

鏈表長度能達到8的概率極小,小於千萬分之一。因爲如果 hashCode 分佈良好,也就是 hash 計算的結果離散好的話,很少出現鏈表很長的情況,這個8就這麼定了。

總結:

通常如果 hash 算法正常的話,那麼鏈表的長度也不會很長,那麼紅黑樹也不會帶來明顯的查詢時間上的優勢,反而會增加空間負擔。所以通常情況下,並沒有必要轉爲紅黑樹,所以就選擇了概率非常小,小於千萬分之一概率,也就是長度爲 8 的概率,把長度 8 作爲轉化的默認閾值。

所以如果平時開發中發現 HashMap 或是 ConcurrentHashMap 內部出現了紅黑樹的結構,這個時候往往就說明我們的哈希算法出了問題,需要留意是不是我們實現了效果不好的 hashCode 方法,並對此進行改進,以便減少衝突

線程安全問題

HashMap是非同步的,線程不安全,也可以通過Collections.synchronizedMap()方法來得到一個同步的HashMap

HashMap存取速度更快,效率高

請參考hashmap 線程安全問題分析

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