1. 程式人生 > >HashMap原理及衝突解決辦法

HashMap原理及衝突解決辦法

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中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,擴容是需要進行陣列複製的,複製陣列是非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。