HashMap的底層實現以及解決hash值衝突的方式

class HashMap<K,V> extends AbstractMap<K,V>

HashMap put()
HashMap get()
1.put()

HashMap put()方法源碼如下:

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;  
            //判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那麼新值覆蓋原來的舊值,並返回舊值。  
            //如果存在相同的hashcode,那麼他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了hash衝突。  
            //Hash衝突後,那麼HashMap的單個bucket裏存儲的不是一個 Entry,而是一個 Entry 鏈。  
            //系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 爲止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),  
            //那系統必須循環到最後才能找到該元素。  
            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;  
    } 

hash值衝突是發生在put()時,從源碼可以看出,hash值是通過hash(key.hashCode())來獲取的,當put的元素越來越多時,難免或出現不同的key產生相同的hash值問題,也即是hash衝突,當拿到一個hash值,通過indexFor(hash, table.length)獲取數組下標,先查詢是否存在該hash值,若不存在,則直接以Entry<V,V>的方式存放在數組中,若存在,則再對比key是否相同,若hash值和key都相同,則替換value,若hash值相同,key不相同,則形成一個單鏈表,將hash值相同,key不同的元素以Entry<V,V>的方式存放在鏈表中,這樣就解決了hash衝突,這種方法叫做分離鏈表法,與之類似的方法還有一種叫做 開放定址法,開放定址法師採用線性探測(從相同hash值開始,繼續尋找下一個可用的槽位)hashMap是數組,長度雖然可以擴大,但用線性探測法去查詢槽位查不到時怎麼辦?因此hashMap採用了分離鏈表法。

2.get()

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

有了上面存儲時的hash算法作爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

當hashMap沒出現hash衝突時,沒有形成單向鏈表,get方法能夠直接定位到元素,但是,出現衝突後,形成了單向鏈表,bucket裏存放的不再是一個entry對象,而是一個entry對象鏈,系統只能順序的遍歷每個entry直到找到想要搜索的entry爲止,這時,問題就來了,如果恰好要搜索的entry位於該entry鏈的最末端,那循環必須要進行到最後一步才能找到元素,此時涉及到一個負載因子的概念,hashMap默認的負載因子爲0.75,這是考慮到存儲空間和查詢時間上成本的一個折中值,增大負載因子,可以減少hash表(就是那個entry數組)所佔用的內空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的操作(put()和get()都用到查詢);減小負載因子,會提高查詢時間,但會增加hash表所佔的內存空間。

結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是容量的兩倍:

3.hashMap數組擴容

當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因爲數組的長度是固定的。所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。

那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中元素個數超過160.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,擴容是需要進行數組複製的,複製數組是非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。

有關負載因子的概念 參考:https://www.cnblogs.com/yesiamhere/p/6653135.html

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