參考:http://zhangshixi.iteye.com/blog/672697java
HashMap是基於哈希表的Map接口的非同步實現。此實現提供全部可選的映射操做,並容許使用null值和null鍵,存儲的對象是一個鍵值對對象(Entry<K,V>)。此類不保證映射的順序,特別是它不保證該順序恆久不變。
注意這裏研究的是JDK7以前的版本,JDK8HashMap採用的是數組+鏈表+紅黑樹的形式。算法
基於數組和鏈表實現,內部維護着一個數組table,該數組保存着每一個鏈表的表頭結點;查找時,先經過hash函數計算key的hash值,再根據key的hash值計算數組索引(取餘法),而後根據索引找到鏈表表頭結點,而後遍歷查找該鏈表。
示意圖以下,左邊的數組索引是根據key的hash值計算獲得,不一樣hash值有可能產生同樣的索引,即哈希衝突,此時採用鏈地址法處理哈希衝突,即將全部索引一致的節點構成一個單鏈表;
數組
源碼以下:緩存
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
能夠看出,Entry就是數組中的元素,每一個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。安全
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記錄HashMap中修改結構的次數 modCount++; // 將key、value添加到i索引處。 addEntry(hash, key, value, i); return null; }
從上面的源代碼中能夠看出:當咱們往HashMap中put元素的時候,先根據key的hashCode從新計算hash值,根據hash值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,剛剛加入的Entry在鏈頭,最早加入的在鏈尾(這一點從addEntry(hash, key, value, i)函數能夠看出來,把新加入的Entry對象放在數組table[i]位置,此Entry的next值指向之前的Entry)。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度依然爲O(1),由於最新的Entry會插入鏈表頭部,只須要簡單改變引用鏈便可,而對於查找操做來說,此時就須要遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。數據結構
addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法(就是沒有public,protected,private這三個訪問權限修飾詞修飾,爲默認的訪問權限,用default表示,但在代碼中沒有這個default),代碼以下:併發
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); }
當系統決定存儲HashMap中的key-value對時,徹底沒有考慮Entry中的value,僅僅只是根據key來計算並決定每一個Entry的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。
hash(int h)方法根據key的hashCode從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的hash衝突。函數
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
上面的很很差記,下面給出JDK8中的實現(僅僅是key的hash值高16位和低16位異或操做):性能
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
咱們能夠看到在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) 方法的代碼以下:優化
static int indexFor(int h, int length) { return h & (length-1); }
這個方法很是巧妙,它經過 h & (table.length -1) 來獲得該對象的保存位,而HashMap底層數組的長度老是 2 的 n 次方,這是HashMap在速度上的優化。在 HashMap 構造器中有以下代碼:
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
這段代碼保證初始化時HashMap的容量老是2的n次方,即底層數組的長度老是爲2的n次方。當length老是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,可是&比%具備更高的效率。
這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:
假設數組長度分別爲15和16,優化後的hash碼分別爲8和9,那麼&運算後的結果以下:
h & (table.length-1) hash table.length-1 8 & (15-1): 1000 & 1110 = 1000 9 & (15-1): 1001 & 1110 = 1000 ----------------------------------------------------------------------------------------------------------------------- 8 & (16-1): 1000 & 1111 = 1000 9 & (16-1): 1001 & 1111 = 1001 -----------------------------------------------------------------------------------------------------------------------
從上面的例子中能夠看出:當八、9兩個數和\((15-1)_2\)=(1110)進行「與運算&」的時候,產生了相同的結果,都爲1000,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲15的時候,hash值會與\((15-1)_2\)=(1110)進行「與運算&」,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101(注意沒有1111,由於數組長度15,最大下標14)這幾個位置永遠都不能存放元素了,空間浪費至關大。也就是數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!
而當數組長度爲16時,即爲2的n次方時,\(2^n\)-1獲得的二進制數的每一個位上的值都爲1(好比\((2^4-1)_2\)=1111),這使得在低位上&時,獲得的和原hash的低位相同,加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值纔會被放到數組中的同一個位置上造成鏈表。
因此說,當數組長度爲2的n次冪的時候,不一樣的key算得的index相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
而且擴容的時候沒必要所有從新計算hash,只須要判斷最高位。
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 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,再根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當須要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。
當HashMap中的元素愈來愈多的時候,hash衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操做也會出如今ArrayList中,這是一個經常使用的操做,而在HashMap數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是resize。
那麼HashMap何時進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor
時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲16,那麼當HashMap中元素個數超過16*0.75=12
(這個值就是代碼中的threshold值,也叫作臨界值)的時候,就把數組的大小擴展爲 2*16=32
,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。HashMap擴容的代碼以下所示:
//HashMap數組擴容 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //若是當前的數組長度已經達到最大值,則再也不進行調整 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //根據傳入參數的長度定義新的數組 Entry[] newTable = new Entry[newCapacity]; //按照新的規則,將舊數組中的元素轉移到新數組中 transfer(newTable); table = newTable; //更新臨界值 threshold = (int)(newCapacity * loadFactor); } //舊數組中元素往新數組中遷移 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 包含以下幾個構造器:
負載因子衡量的是一個散列表空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。
HashMap的實現中,經過threshold字段來判斷HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下容許的最大元素數目,超過這個數目就從新resize,以下降實際的負載因子(也就是說雖然數組長度是capacity,但其擴容的臨界值倒是threshold)。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是容量的兩倍:
if (size++ >= threshold) resize(2 * table.length);
咱們知道java.util.HashMap不是線程安全的,所以若是在使用迭代器的過程當中有其餘線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
這一策略在源碼中的實現是經過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增長這個值,那麼在迭代器初始化過程當中會將這個值賦給迭代器的expectedModCount。在迭代過程當中,判斷modCount跟expectedModCount是否相等,若是不相等就表示已經有其餘線程修改了Map:注意到modCount聲明爲volatile,保證線程之間修改的可見性。(volatile之因此線程安全是由於被volatile修飾的變量不保存緩存,直接在內存中修改,所以可以保證線程之間修改的可見性)。
在HashMap的API中指出:
由全部HashMap類的「collection 視圖方法」所返回的迭代器都是快速失敗的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會快速失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。
在迭代器建立以後,其視圖中元素已肯定,而這個時候,若是外界經過其餘任何方式修改此試圖,都將致使迭代結果的不一致性,所以這種快速失敗行爲能夠有效的避免面對併發修改時帶來的不肯定風險。
所以,反過來講,迭代器的這種快速失敗行爲所拋出的異常,並不是是提供給調用者去處理的異常,而是用於檢測程序錯誤。
// 內部class HashIterator迭代器 private abstract class HashIterator<E> implements Iterator<E> { Entry<K, V> next; // 下一個桶 int expectedModCount; // 保護HashMap沒有變動 int index; // 當前的索引 Entry<K, V> current; // 當前的桶 // 構造方法 HashIterator() { // 保存modCount,由於若是HashMap進行了任何操做modCount都會增長,因此若是發現modCount變化了,就能夠拋出失敗 expectedModCount = modCount; // 進入第一個桶 if (size > 0) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } // 看看有沒有下一個桶 public final boolean hasNext() { return next != null; } // 獲取下一個桶 final Entry<K, V> nextEntry() { // modCount變化了,拋出失敗 if (modCount != expectedModCount) throw new ConcurrentModificationException(); // 獲得next Entry<K, V> e = next; // 若是next爲空,拋出失敗 if (e == null) throw new NoSuchElementException(); // 若是next.next爲空,將next定義爲下一個格子中的桶,不然爲該格子的下一個桶 if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } // 給current賦值 current = e; // 返回e return e; } // 刪除 public void remove() { // 若是當前爲空,拋出 if (current == null) throw new IllegalStateException(); // modCount變化了,拋出失敗 if (modCount != expectedModCount) throw new ConcurrentModificationException(); // 得到當前的key Object k = current.key; // 設置current爲null current = null; // 刪除掉對應key的元素 HashMap.this.removeEntryForKey(k); // 重置expectedModCount expectedModCount = modCount; } } // 內部class ValueIterator迭代器,咱們能夠看到修改了next方法 private final class ValueIterator extends HashIterator<V> { public V next() { return nextEntry().value; } } // 內部class KeyIterator迭代器,咱們能夠看到修改了next方法 private final class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } } // 內部class EntryIterator迭代器,咱們能夠看到修改了next方法 private final class EntryIterator extends HashIterator<Map.Entry<K, V>> { public Map.Entry<K, V> next() { return nextEntry(); } }
一直到JDK7爲止,HashMap的結構都是這麼簡單,基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式存儲。
若是成百上千個節點在hash時發生碰撞,存儲在一個鏈表中,那麼若是要查找其中一個節點,那就不可避免的花費O(N)的查找時間,這將是多麼大的性能損失。這個問題終於在JDK8中獲得瞭解決。紅黑樹查找複雜度O(logn),這樣會提升HashMap的效率。當鏈表中元素個數超過默認設定(8個)時,就會採用下面的結構: