HashMap的put、get方法分析與Hash衝突的分析、解決

1HashMap的實現原理
簡單地說,HashMap就是將key做hash算法,然後將hash值映射到內存地址,直接取得key所對應的數據。在HashMap中,底層數據結構使用的是數組,所謂的內存地址即數組的下標索引。afHashMap的高性能需要保證以下幾點:
  • hash算法必須是高效的
  • hash值到內存地址(數組索引)的算法是快速的
  • 根據內存地址(數組索引)可以直接取得對應的值
首先來看第一點,hash算法的高效性。在HashMap中,hash算法有關的代碼如下:
1 int hash = hash(key.hashCode());
2 public native int hashCode();
3 static int hash(int h) {
4 h ^= (h >>> 20) ^ (h >>> 12);
5 return h ^ (h >>> 7) ^ (h >>> 4);
6 }
第一行代碼是HashMap中用於計算key的hash值。它前後分別調用了Object類的hashCode()方法和HashMap的內部函數hash()。Object類的hashCode()方法默認是native的實現,可以認爲不存在性能問題。而hash()函數的實現全部基於位運算,因此,也是高效的。
注意:native方法通常比一般的方法快,因爲它直接調用操作系統本地鏈接庫的API。由於hashCode()方法是可以重載的,因此,爲了保證HashMap的性能,需要確保相關的hashCode()是高效的。而位運算也比算術、邏輯運算快。
當取得key的hash值後,需要通過hash值得到內存地址:
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor()函數通過將hash值和數組長度按位取與直接得到數組索引。
最後由indexFor()函數返回的數組索引直接通過數組下標便可取得對應的值。直接的內存訪問速度也相當快。因此,可以認爲HashMap是高性能的。
2Hash衝突
雖然上節中闡述了在理想情況下HashMap的高效性,但我們依然不得不在實際使用中考慮HashMap的一些特殊情況,這些情況有可能給HashMap帶來一定的性能問題。其中,最值得關注便是hash衝突。如圖3.11所示,需要存放到HashMap中的兩個元素1和2,通過hash計算後,發現對應在內存中的同一個地址。此時,HashMap又會如何處理,以保證數據可以完整存放,並正常工作呢?
3.11 Hash衝突示意圖
要處理好這個問題,需要進一步深入HashMap,雖然HashMap的底層實現使用的是數組,但是數組內的元素並不是簡單的值,而是一個Entry類的對象。因此,對HashMap結構的貼切描述如圖3.12所示。
3.12 HashMap表項結構
可以看到,HashMap的內部維護着一個Entry數組,每一個Entry表項包括key、value、next和hash幾項。這裏特別注意其中的next部分,他指向了另外一個Entry(所以實際上HashMap的數據結構是一個列表數組,數組中每個元素是一個Entry列表,Entry列表中每個元素是一個Entry對象)。進一步閱讀HashMap的put()方法源碼,可以看到當put()操作有衝突時,新的Entry依然會被安放在對應的索引下標內,並替換原有的值。同時,爲了保證舊值不丟失,會將新的Entry的next指向舊值。這便實現了在一個數組索引空間內,存放多個值項。因此,如圖3.12所示,HashMap實際上是一個鏈表爲元素的數組。
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;
        //如果當前的key已經存在於HashMap
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;            //取得舊值
            e.value = value;
            e.recordAccess(this);
            return oldValue;                //返回舊值
        }
    }
    modCount++;
    addEntry(hash, key, value, i);                //添加當前的表項到i位置
    return null;
}
addEntry()方法的實現如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//將新增元素放到i的位置,並讓它的next指向舊的元素
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
        resize(2 * table.length);
}
基於HashMap的這種實現機制,只要hashCode()和hash()方法實現的足夠好,能夠儘可能的減少衝突的產生,那麼對HashMap的操作幾乎等價於對數組的隨機訪問操作,具有很好的性能。但是,如果hashCode()或者hash()方法實現較差,在大量衝突產生的情況下,HashMap事實上就退化爲幾個鏈表,對HashMap的操作等價於遍歷鏈表,此時性能很差。
考慮一個在極端情況下的例子,假設類BadHash有着一個很槽糕的hashCode()實現:
    public class BadHash{
        double d;
        public BadHash(double d){
            this.d=d;
        }
        @Override
        public int hashCode(){
            return 1;                        //一個槽糕的hashCode()實現
        }
    }
類GoodHash擁有默認hashCode()方法:
    public class GoodHash{
        double d;
        public GoodHash(double d){
            this.d=d;
        }
    }
分別使用BadHash類和GoodHash類作爲HashMap的key,產生1萬一個對象並將其存入HashMap中,執行get()方法1萬次。結果BadHash類相對耗時1297ms,而GoodHash類僅耗時15ms。這正是隨機數據訪問和鏈表遍歷的性能差距。


再補充一下HashMap的get(key)方法的實現原理解析:

public V get(Object key) {
if (key == null) // 鍵爲null的Entry是tables[0]
return getForNullKey();
// 首先根據key獲取hash值,跟put方法一致
int hash = hash(key.hashCode());
// 同樣通過位與運算獲取Entry[] tables下標
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)))
// 當匹配到hash值相同,key相同的Entry元素時,返回Entry對象的vlaue
return e.value;
}
return null;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章