HashMap源碼學習總結

什麼是Map?

Map用於保存具有key-value映射關係的數據

首先看圖!

這裏寫圖片描述

可以看出Java 中有四種常見的Map實現——HashMap, TreeMap, Hashtable和LinkedHashMap:

·HashMap就是一張hash表,鍵和值都沒有排序。
·TreeMap以紅黑樹結構爲基礎,鍵值可以設置按某種順序排列。
·LinkedHashMap保存了插入時的順序。
·Hashtable是同步的(而HashMap是不同步的)。所以如果在線程安全的環境下應該多使用HashMap,而不是Hashtable,因爲Hashtable對同步有額外的開銷。

我們在這裏簡單的說說HashMap:

(1)HashMap是基於哈希表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

(2)HashMap是非線程安全的,只用於單線程環境下,多線程環境下可以採用concurrent併發包下的concurrentHashMap。

(3)HashMap 實現了Serializable接口,因此它支持序列化。

(4)HashMap還實現了Cloneable接口,故能被克隆。


先從HashMap的存儲結構說起:

這裏寫圖片描述

藍色部分即代表哈希表本身(其實是一個數組),數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決hash地址衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中保存。

HashMap的構造方法中有兩個很重要的參數:初始容量和加載因子

這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是創建哈希表時的容量(默認爲16),加載因子是哈希表當前key的數量和容量的比值,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表提前進行 resize 操作(即擴容)。如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那麼表中的數據將過於稀疏(很多空間還沒用,就開始擴容了),嚴重浪費。

JDK開發者規定的默認加載因子爲0.75,因爲這是一個比較理想的值。另外,無論指定初始容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的冪次方,且最大值不能超過2的30次方。

我們來分析一下HashMap中用的最多的兩個方法put和get的源碼

get():

// 獲取key對應的value
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        // 獲取key的hash值
        int hash = hash(key.hashCode());
        // 在“該hash值對應的鏈表”上查找“鍵值等於key”的元素
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            // 判斷key是否相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        // 沒找到則返回null
        return null;
    }

    // 獲取“key爲null”的元素的值,HashMap將“key爲null”的元素存儲在table[0]位置,但不一定是該鏈表的第一個位置!
    private V getForNullKey() {
        for (Entry<K, V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

首先,如果key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中,當然不一定是存放在頭結點table[0]中。如果key不爲null,則先求的key的hash值,根據hash值找到在table中的索引,在該索引對應的單鏈表中查找是否有鍵值對的key與目標key相等,有就返回對應的value,沒有則返回null。

put():

// 將“key-value”添加到HashMap中
    public V put(K key, V value) {
        // 若“key爲null”,則將該鍵值對添加到table[0]中。
        if (key == null)
            return putForNullKey(value);
        // 若“key不爲null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
        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”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        // 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中
        modCount++;
        // 將key-value添加到table[i]處
        addEntry(hash, key, value, i);
        return null;
    }

如果key爲null,則將其添加到table[0]對應的鏈表中,如果key不爲null,則同樣先求出key的hash值,根據hash值得出在table中的索引,而後遍歷對應的單鏈表,如果單鏈表中存在與目標key相等的鍵值對,則將新的value覆蓋舊的value,且將舊的value返回,如果找不到與目標key相等的鍵值對,或者該單鏈表爲空,則將該鍵值對插入到單鏈表的頭結點位置(每次新插入的節點都是放在頭結點的位置),該操作是有addEntry方法實現的,它的源碼如下:

// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 保存“bucketIndex”位置的值到“e”中
        Entry<K, V> e = table[bucketIndex];
        // 設置“bucketIndex”位置的元素爲“新Entry”,
        // 設置“e”爲“新Entry的下一個節點”
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小
        if (size++ >= threshold)
            resize(2 * table.length);
    }

注意這裏倒數第三行的構造方法,將key-value鍵值對賦給table[bucketIndex],並將其next指向元素e,這便將key-value放到了頭結點中,並將之前的頭結點接在了它的後面。該方法也說明,每次put鍵值對的時候,總是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。兩外注意最後兩行代碼,每次加入鍵值對時,都要判斷當前已用的槽的數目是否大於等於閥值(容量*加載因子),如果大於等於,則進行擴容,將容量擴爲原來容量的2倍。

接下來重點來分析下求hash值和索引值的方法,這兩個方法便是HashMap設計的最爲核心的部分,二者結合能保證哈希表中的元素儘可能均勻地散列。

由hash值找到對應索引的方法如下:

static int indexFor(int h, int length) {
        return h & (length-1);
     }

因爲容量初始還是設定都會轉化爲2的冪次。故可以使用高效的位與運算替代模運算。

計算hash值的方法如下:

 static int hash(int h) {
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

JDK 的 HashMap 使用了一個 hash 方法對hash值使用位的操作,使hash值的計算效率很高。爲什麼這樣做?主要是因爲如果直接使用hashcode值,那麼這是一個int值(8個16進制數,共32位),int值的範圍正負21億多,但是hash表沒有那麼長,一般比如初始16,自然散列地址需要對hash表長度取模運算,得到的餘數纔是地址下標。假設某個key的hashcode是0AAA0000,hash數組長默認16,如果不經過hash函數處理,該鍵值對會被存放在hash數組中下標爲0處,因爲0AAA0000 & (16-1) = 0。過了一會兒又存儲另外一個鍵值對,其key的hashcode是0BBB0000,得到數組下標依然是0,這就說明這是個實現得很差的hash算法,因爲hashcode的1位全集中在前16位了,導致算出來的數組下標一直是0。於是明明key相差很大的鍵值對,卻存放在了同一個鏈表裏,導致以後查詢起來比較慢(蛻化爲了順序查找)。故JDK的設計者使用hash函數的若干次的移位、異或操作,把hashcode的“1位”變得“鬆散”,非常巧妙。

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