【源碼分析】深入理解HashMap 學習手記

本文基於JDK1.6

 

HashMap簡介

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

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

    HashMap 實現了Serializable接口,因此它支持序列化,實現了Cloneable接口,能被克隆。

 

HashMap的存儲結構

    圖中,紫色部分即代表哈希表,也稱爲哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。

  鏈表中節點的數據結構

    // Entry是單向鏈表。  
    // 它是 “HashMap鏈式存儲法”對應的鏈表。  
    // 它實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數  
    static class Entry<K,V> implements Map.Entry<K,V> {  
        final K key;  
        V value;  
        // 指向下一個節點  
        Entry<K,V> next;  
        final int hash;  
 
        // 構造函數。  
        // 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)"  
        Entry(int h, K k, V v, Entry<K,V> n) {  
            value = v;  
            next = n;  
            key = k;  
            hash = h;  
        }  
 
        public final K getKey() {  
            return key;  
        }  
 
        public final V getValue() {  
            return value;  
        }  
 
        public final V setValue(V newValue) {  
            V oldValue = value;  
            value = newValue;  
            return oldValue;  
        }  
 
        // 判斷兩個Entry是否相等  
        // 若兩個Entry的“key”和“value”都相等,則返回true。  
        // 否則,返回false  
        public final boolean equals(Object o) {  
            if (!(o instanceof Map.Entry))  
                return false;  
            Map.Entry e = (Map.Entry)o;  
            Object k1 = getKey();  
            Object k2 = e.getKey();  
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {  
                Object v1 = getValue();  
                Object v2 = e.getValue();  
                if (v1 == v2 || (v1 != null && v1.equals(v2)))  
                    return true;  
            }  
            return false;  
        }  
 
        // 實現hashCode()  
        public final int hashCode() {  
            return (key==null   ? 0 : key.hashCode()) ^  
                   (value==null ? 0 : value.hashCode());  
        }  
 
        public final String toString() {  
            return getKey() + "=" + getValue();  
        }  
 
        // 當向HashMap中添加元素時,繪調用recordAccess()。  
        // 這裏不做任何處理  
        void recordAccess(HashMap<K,V> m) {  
        }  
 
        // 當從HashMap中刪除元素時,繪調用recordRemoval()。  
        // 這裏不做任何處理  
        void recordRemoval(HashMap<K,V> m) {  
        }  
    }  

 它的結構元素除了key、value、hash外,還有next,next指向下一個節點。另外,這裏覆寫了equals和hashCode方法來保證鍵值對的獨一無二。

 

    HashMap共有四個構造方法

// 指定“容量大小”和“加載因子”的構造函數  
    public HashMap(int initialCapacity, float loadFactor) {  
        if (initialCapacity < 0)  
            throw new IllegalArgumentException("Illegal initial capacity: " +  
                                               initialCapacity);  
        // HashMap的最大容量只能是MAXIMUM_CAPACITY  
        if (initialCapacity > MAXIMUM_CAPACITY)  
            initialCapacity = MAXIMUM_CAPACITY;  
		//加載因此不能小於0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  
            throw new IllegalArgumentException("Illegal load factor: " +  
                                               loadFactor);  
 
        // 找出“大於initialCapacity”的最小的2的冪  
        int capacity = 1;  
        while (capacity < initialCapacity)  
            capacity <<= 1;  
 
        // 設置“加載因子”  
        this.loadFactor = loadFactor;  
        // 設置“HashMap閾值”,當HashMap中存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。  
        threshold = (int)(capacity * loadFactor);  
        // 創建Entry數組,用來保存數據  
        table = new Entry[capacity];  
        init();  
    }  
 
 
    // 指定“容量大小”的構造函數  
    public HashMap(int initialCapacity) {  
        this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    }  
 
    // 默認構造函數。  
    public HashMap() {  
        // 設置“加載因子”爲默認加載因子0.75  
        this.loadFactor = DEFAULT_LOAD_FACTOR;  
        // 設置“HashMap閾值”,當HashMap中存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。  
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
        // 創建Entry數組,用來保存數據  
        table = new Entry[DEFAULT_INITIAL_CAPACITY];  
        init();  
    }  
 
    // 包含“子Map”的構造函數  
    public HashMap(Map<? extends K, ? extends V> m) {  
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
        // 將m中的全部元素逐個添加到HashMap中  
        putAllForCreate(m);  
    }  

構造方法中提到了兩個很重要的參數:初始容量和加載因子。這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是創建哈希表時的容量(從構造函數中可以看出,如果不指明,則默認爲16),加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 resize 操作(即擴容)。

    下面說下加載因子,如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那麼表中的數據將過於稀疏(很多空間還沒用,就開始擴容了),對空間造成嚴重浪費。如果我們在構造方法中不指定,則系統默認加載因子爲0.75,這是一個比較理想的值,一般情況下我們是無需修改的。

    另外,無論我們指定的容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的次方的一個數,且最大值不能超過2的30次方

 

 HashMap中key和value都允許爲null。

 

  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]對應的鏈表中,putForNullKey的源碼如下:

    // putForNullKey()的作用是將“key爲null”鍵值對添加到table[0]位置  
    private V putForNullKey(V value) {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        // 如果沒有存在key爲null的鍵值對,則直接題阿見到table[0]處!  
        modCount++;  
        addEntry(0, null, value, 0);  
        return null;  
    } 

 如果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倍。

 

HashMap的擴容

resize方法

    // 重新調整HashMap的大小,newCapacity是調整後的單位  
    void resize(int newCapacity) {  
        Entry[] oldTable = table;  
        int oldCapacity = oldTable.length;  
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
 
        // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中,  
        // 然後,將“新HashMap”賦值給“舊HashMap”。  
        Entry[] newTable = new Entry[newCapacity];  
        transfer(newTable);  
        table = newTable;  
        threshold = (int)(newCapacity * loadFactor);  
    }  

很明顯,是新建了一個HashMap的底層數組,而後調用transfer方法,將就HashMap的全部元素添加到新的HashMap中(要重新計算元素在新的數組中的索引位置)。transfer方法的源碼如下:

    // 將HashMap中的全部元素都添加到newTable中  
    void transfer(Entry[] newTable) {  
        Entry[] src = table;  
        int newCapacity = newTable.length;  
        for (int j = 0; j < src.length; j++) {  
            Entry<K,V> e = src[j];  
            if (e != null) {  
                src[j] = null;  
                do {  
                    Entry<K,V> next = e.next;  
                    int i = indexFor(e.hash, newCapacity);  
                    e.next = newTable[i];  
                    newTable[i] = e;  
                    e = next;  
                } while (e != null);  
            }  
        }  
    }  

很明顯,擴容是一個相當耗時的操作,因爲它需要重新計算這些元素在新的數組中的位置並進行復制處理。因此,我們在用HashMap的時,最好能提前預估下HashMap中元素的個數,這樣有助於提高HashMap的性能。

 

   注意containsKey方法和containsValue方法。前者直接可以通過key的哈希值將搜索範圍定位到指定索引對應的鏈表,而後者要對哈希數組的每個鏈表進行搜索。

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

計算哈希值的方法

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

 它只是一個數學公式,IDK這樣設計對hash值的計算,自然有它的好處,至於爲什麼這樣設計,我們這裏不去追究,只要明白一點,用的位的操作使hash值的計算效率很高。

 

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

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

 這個我們要重點說下,我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap中則通過h&(length-1)的方法來代替取模,同樣實現了均勻的散列,但效率要高很多,這也是HashMap對Hashtable的一個改進。

 

    接下來,我們分析下爲什麼哈希表的容量一定要是2的整數次冪。首先,length爲2的整數次冪的話,h&(length-1)就相當於對length取模,這樣便保證了散列的均勻,同時也提升了效率;其次,length爲2的整數次冪的話,爲偶數,這樣length-1爲奇數,奇數的最後一位是1,這樣便保證了h&(length-1)的最後一位可能爲0,也可能爲1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣便可以保證散列的均勻性,而如果length爲奇數的話,很明顯length-1爲偶數,它的最後一位是0,這樣h&(length-1)的最後一位肯定爲0,即只能爲偶數,這樣任何hash值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,因此,length取2的整數次冪,是爲了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。

 

 

文章摘自   http://blog.csdn.net/ns_code/article/details/36034955

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