Java HashMap 源碼分析__Java

來源:互聯網
上載者:User

一、HashMap概述

HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。


二、HashMap的資料結構
在java程式設計語言中,最基本的結構就是兩種,一個是數組,另外一個是鏈表(引用),所有的資料結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的資料結構,即數組和鏈表的結合體。





從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當建立一個HashMap的時候,就會初始化一個數組。


本文將結合JDK7 中的HashMap 原始碼來進行分析,以下是源碼片段:


public class HashMap<K,V>    extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable{    /**     * The default initial capacity - MUST be a power of two.     */    static final int DEFAULT_INITIAL_CAPACITY = 16;    /**     * The maximum capacity, used if a higher value is implicitly specified     * by either of the constructors with arguments.     * MUST be a power of two <= 1<<30.     */    static final int MAXIMUM_CAPACITY = 1 << 30;


可以看出HashMap 預設的負載容量為16,負載因子為 0.75。

存放索引值對Entry 數組 table

    /**     * The load factor used when none specified in constructor.     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;    /**     * The table, resized as necessary. Length MUST Always be a power of two.     */    transient Entry<K,V>[] table;


再看看Entry 類的定義

    static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        int hash;        /**         * Creates new entry.         */        Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;            key = k;            hash = h;        }


可以看出,Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。



HashMap構造方法

    public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        // Find a power of 2 >= initialCapacity        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        table = new Entry[capacity];        useAltHashing = sun.misc.VM.isBooted() &&                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);        init();    }


三、HashMap的存取實現

1、儲存

public V put(K key, V value) {      // HashMap允許存放null鍵和null值。      // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。      if (key == null)          return putForNullKey(value);      // 根據key的keyCode重新計算hash值。      int hash = hash(key.hashCode());      // 搜尋指定hash值在對應table中的索引。      int i = indexFor(hash, table.length);      // 如果 i 索引處的 Entry 不為 null,通過迴圈不斷遍曆 e 元素的下一個元素。      for (Entry<K,V> e = table[i]; e != null; e = e.next) {          Object k;          if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {              V oldValue = e.value;              e.value = value;              e.recordAccess(this);              return oldValue;          }      }      // 如果i索引處的Entry為null,表明此處還沒有Entry。      modCount++;      // 將key、value添加到i索引處。      addEntry(hash, key, value, i);      return null;  }  

從上面的原始碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在數組table的i索引處,代碼如下:

void addEntry(int hash, K key, V value, int bucketIndex) {      // 擷取指定 bucketIndex 索引處的 Entry       Entry<K,V> e = table[bucketIndex];      // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry      table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      // 如果 Map 中的 key-value 對的數量超過了極限      if (size++ >= threshold)      // 把 table 對象的長度擴充到原來的2倍。          resize(2 * table.length);  }


根據上面 put 方法的原始碼可以看出,當程式試圖將一個key-value對放入HashMap中時,程式首先根據該 key 的 hashCode() 傳回值決定該 Entry 的儲存位置:如果兩個 Entry 的 key 的 hashCode() 傳回值相同,那它們的儲存位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部。

hash(Object k)方法根據key的hashCode重新計算一次散列。此演算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。 

final int hash(Object k) {        int h = 0;        if (useAltHashing) {            if (k instanceof String) {                return sun.misc.Hashing.stringHash32((String) k);            }            h = hashSeed;        }        h ^= k.hashCode();        // This function ensures that hashCodes that differ only by        // constant multiples at each bit position have a bounded        // number of collisions (approximately 8 at default load factor).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }

我們可以看到在HashMap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash演算法。前面說過HashMap的資料結構是數組和鏈表的結合,所以我們當然希望這個HashMap裡面的 元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍曆鏈表,這樣就大大最佳化了查詢的效率。


對於任意給定的對象,只要它的 hashCode() 傳回值相同,那麼程式調用 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把hash值對數組長度模數運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在HashMap中是這樣做的:調用 indexFor(int h, int length) 方法來計算該對象應該儲存在 table 數組的哪個索引處。indexFor(int h, int length) 方法的代碼如下:

/**     * Returns index for hash code h.     */    static int indexFor(int h, int length) {        return h & (length-1);    }


這個方法非常巧妙,它通過 h & (table.length -1) 來得到該對象的儲存位,而HashMap底層數組的長度總是 2 的 n 次方,這是HashMap在速度上的最佳化。在 HashMap 構造器中有如下代碼:

// Find a power of 2 >= initialCapacity        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;

這段代碼保證初始化時HashMap的容量總是2的n次方,即底層數組的長度總是為2的n次方。
當length總是 2 的n次方時,h& (length-1)運算等價於對length模數,也就是h%length,但是&比%具有更高的效率。

2、取值

public V get(Object key) {        if (key == null)            return getForNullKey();        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue();    }

getForNullKey()方法如下:

private V getForNullKey() {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null)                return e.value;        }        return null;    }


getEntry(Object key)方法如下:

/**     * Returns the entry associated with the specified key in the     * HashMap.  Returns null if the HashMap contains no mapping     * for the key.     */    final Entry<K,V> getEntry(Object key) {        int hash = (key == null) ? 0 : hash(key);        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 != null && key.equals(k))))                return e;        }        return null;    }


有了上面儲存時的hash演算法作為基礎,理解起來這段代碼就很容易了。從上面的原始碼中可以看出:從HashMap中get元素時,首先判斷key是否為null值,如果是null值則直接返回getForNullKey() (table數組位置為0處的第一個key為null的值),若key不為空白,則 計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。


簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來儲存所有的 key-value 對,當需要儲存一個 Entry 對象時,會根據hash演算法來決定其在數組中的儲存位置,在根據equals方法決定其在該數組位置上的鏈表中的儲存位置;當需要取出一個Entry時,也會根據hash演算法找到其在數組中的儲存位置,再根據equals方法從該位置上的鏈表中取出該Entry。



四、HashMap的rehash(resize)

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

那麼HashMap什麼時候進行擴容呢。可以查看一下原始碼

void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }


threshold的定義如下:
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);


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


五、影響HashMap效能的參數
HashMap 包含如下幾個構造器:

HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。
HashMap的基礎構造器HashMap(int initialCapacity, float loadFactor)帶有兩個參數,它們是初始容量initialCapacity和載入因子loadFactor,代碼如下:

public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        // Find a power of 2 >= initialCapacity        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        table = new Entry[capacity];        useAltHashing = sun.misc.VM.isBooted() &&                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);        init();    }


initialCapacity:HashMap的最大容量,即為底層數組的長度。 loadFactor:負載因子loadFactor定義為:散列表的實際元素數目(n)/ 散列表的容量(m)。 負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,尋找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是尋找效率的降低;如果負載因子太小,那麼散列表的資料將過於稀疏,對空間造成嚴重浪費。


HashMap的實現中,通過threshold欄位來判斷HashMap的最大容量

threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

threshold為capacity * loadFactor 和 MAXIMUM_CAPACITY + 1 兩者中的最小值。


HashMap在addEntry(int hash, K key, V value, int bucketIndex)方法中判斷是否需要擴容,如下:

void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }


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


六、Fail-Fast機制

我們知道java.util.HashMap不是安全執行緒的,因此如果在使用迭代器的過程中有其他線程修改了HashMap,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
這一策略在源碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount。

private abstract class HashIterator<E> implements Iterator<E> {        Entry<K,V> next;        // next entry to return        int expectedModCount;   // For fast-fail        int index;              // current slot        Entry<K,V> current;     // current entry        HashIterator() {            expectedModCount = modCount;            if (size > 0) { // advance to first entry                Entry[] t = table;                while (index < t.length && (next = t[index++]) == null)                    ;            }        }


在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map,如下:

final Entry<K,V> nextEntry() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            Entry<K,V> e = next;            if (e == null)                throw new NoSuchElementException();            if ((next = e.next) == null) {                Entry[] t = table;                while (index < t.length && (next = t[index++]) == null)                    ;            }            current = e;            return e;        }


/**     * The number of times this HashMap has been structurally modified     * Structural modifications are those that change the number of mappings in     * the HashMap or otherwise modify its internal structure (e.g.,     * rehash).  This field is used to make iterators on Collection-views of     * the HashMap fail-fast.  (See ConcurrentModificationException).     */    transient int modCount;


注意到modCount聲明為transient,保證線程之間修改的可見度。



七、總結
HashMap基於hashing原理,我們通過put()和get()方法儲存和擷取對象。當我們將索引值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當擷取對象時,通過鍵對象的equals()方法找到正確的索引值對,然後傳回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存索引值對對象。

當兩個不同的鍵對象的hashcode相同時,它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到索引值對。



以下是整理網路上的關於HashMap的一些比較經典問題:
為什麼String, Interger這樣的wrapper類適合作為鍵。 

String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止索引值改變,如果索引值在放入時和擷取時返回不同的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其他的優點如安全執行緒。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那麼請這麼做吧。因為擷取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那麼碰撞的幾率就會小些,這樣就能提高HashMap的效能。

我們可以使用自訂的對象作為鍵嗎。 
這是前一個問題的延伸。當然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當對象插入到Map中之後將不會再改變了。如果這個自訂對象時不可變的,那麼它已經滿足了作為鍵的條件,因為當它建立之後就已經不能改變了。


我們可以使用CocurrentHashMap來代替Hashtable嗎。
這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步效能更好,因為它僅僅根據同步層級對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的執行緒安全性。關於Hashtable和ConcurrentHashMap的區別可以查看這篇部落格:http://javarevisited.blogspot.sg/2011/04/difference-between-concurrenthashmap.html








聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.