JAVA hashmap詳解

1.    HashMap概述:java

   HashMap是基於哈希表的Map接口的非同步實現。此實現提供全部可選的映射操做,並容許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。算法

 

2.    HashMap的數據結構:編程

   java編程語言中,最基本的結構就是兩種,一個是數組,另一個是模擬指針(引用),全部的數據結構均可以用這兩個基本結構來構造的,HashMap也不例外。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。數組

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

   源碼以下:數據結構

 

Java代碼 複製代碼  收藏代碼
  1. /**  
  2.  * The table, resized as necessary. Length MUST Always be a power of two.  
  3.  */  
  4. transient Entry[] table;   
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {   
  7.     final K key;   
  8.     V value;   
  9.     Entry<K,V> next;   
  10.     final int hash;   
  11.     ……   
  12. }  
[java] view plain copy
  1. /** 
  2.  * The table, resized as necessary. Length MUST Always be a power of two. 
  3.  */  
  4. transient Entry[] table;  
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {  
  7.     final K key;  
  8.     V value;  
  9.     Entry<K,V> next;  
  10.     final int hash;  
  11.     ……  
  12. }  

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

 

3.    HashMap的存取實現:編程語言

   1) 存儲:性能

 

Java代碼 複製代碼  收藏代碼
  1. public V put(K key, V value) {   
  2.     // HashMap容許存放null鍵和null值。   
  3.     // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。   
  4.     if (key == null)   
  5.         return putForNullKey(value);   
  6.     // 根據key的keyCode從新計算hash值。   
  7.     int hash = hash(key.hashCode());   
  8.     // 搜索指定hash值在對應table中的索引。   
  9.     int i = indexFor(hash, table.length);   
  10.     // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素。   
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  12.         Object k;   
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  14.             V oldValue = e.value;   
  15.             e.value = value;   
  16.             e.recordAccess(this);   
  17.             return oldValue;   
  18.         }   
  19.     }   
  20.     // 若是i索引處的Entry爲null,代表此處尚未Entry。   
  21.     modCount++;   
  22.     // 將key、value添加到i索引處。   
  23.     addEntry(hash, key, value, i);   
  24.     return null;   
  25. }  
[java] view plain copy
  1. public V put(K key, V value) {  
  2.     // HashMap容許存放null鍵和null值。  
  3.     // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。  
  4.     if (key == null)  
  5.         return putForNullKey(value);  
  6.     // 根據key的keyCode從新計算hash值。  
  7.     int hash = hash(key.hashCode());  
  8.     // 搜索指定hash值在對應table中的索引。  
  9.     int i = indexFor(hash, table.length);  
  10.     // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素。  
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  12.         Object k;  
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             V oldValue = e.value;  
  15.             e.value = value;  
  16.             e.recordAccess(this);  
  17.             return oldValue;  
  18.         }  
  19.     }  
  20.     // 若是i索引處的Entry爲null,代表此處尚未Entry。  
  21.     modCount++;  
  22.     // 將key、value添加到i索引處。  
  23.     addEntry(hash, key, value, i);  
  24.     return null;  
  25. }  

 

   從上面的源代碼中能夠看出:當咱們往HashMapput元素的時候,先根據keyhashCode從新計算hash值,根據hash值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。 優化

   addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在數組tablei索引處。addEntry HashMap 提供的一個包訪問權限的方法,代碼以下:

 

Java代碼 複製代碼  收藏代碼
  1. void addEntry(int hash, K key, V value, int bucketIndex) {   
  2.     // 獲取指定 bucketIndex 索引處的 Entry    
  3.     Entry<K,V> e = table[bucketIndex];   
  4.     // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry   
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   
  6.     // 若是 Map 中的 key-value 對的數量超過了極限   
  7.     if (size++ >= threshold)   
  8.     // 把 table 對象的長度擴充到原來的2倍。   
  9.         resize(2 * table.length);   
  10. }  
[java] view plain copy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     // 獲取指定 bucketIndex 索引處的 Entry   
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry  
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  6.     // 若是 Map 中的 key-value 對的數量超過了極限  
  7.     if (size++ >= threshold)  
  8.     // 把 table 對象的長度擴充到原來的2倍。  
  9.         resize(2 * table.length);  
  10. }  

 

   當系統決定存儲HashMap中的key-value對時,徹底沒有考慮Entry中的value,僅僅只是根據key來計算並決定每一個Entry的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。

   hash(int h)方法根據keyhashCode從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的hash衝突。

 

Java代碼 複製代碼  收藏代碼
  1. static int hash(int h) {   
  2.     h ^= (h >>> 20) ^ (h >>> 12);   
  3.     return h ^ (h >>> 7) ^ (h >>> 4);   
  4. }  
[java] view plain copy
  1. static int hash(int h) {  
  2.     h ^= (h >>> 20) ^ (h >>> 12);  
  3.     return h ^ (h >>> 7) ^ (h >>> 4);  
  4. }  

 

 

   咱們能夠看到在HashMap中要找到某個元素,須要根據keyhash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過HashMap的數據結構是數組和鏈表的結合,因此咱們固然但願這個HashMap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。

   對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 hash 碼值老是相同的。咱們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,運算的消耗仍是比較大的,在HashMap中是這樣作的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:

 

Java代碼 複製代碼  收藏代碼
  1. static int indexFor(int h, int length) {   
  2.     return h & (length-1);   
  3. }  
[java] view plain copy
  1. static int indexFor(int h, int length) {  
  2.     return h & (length-1);  
  3. }  

 

 

   這個方法很是巧妙,它經過 h & (table.length -1) 來獲得該對象的保存位,而HashMap底層數組的長度老是 2 n 次方,這是HashMap在速度上的優化。在 HashMap 構造器中有以下代碼:

Java代碼 複製代碼  收藏代碼
  1. int capacity = 1;   
  2.     while (capacity < initialCapacity)   
  3.         capacity <<= 1;  
[java] view plain copy
  1. int capacity = 1;  
  2.     while (capacity < initialCapacity)  
  3.         capacity <<= 1;  

   這段代碼保證初始化時HashMap的容量老是2n次方,即底層數組的長度老是爲2n次方。

length老是 2 n次方時,h& (length-1)運算等價於對length取模,也就是h%length,可是&%具備更高的效率。

   這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:

   假設數組長度分別爲1516,優化後的hash碼分別爲89,那麼&運算後的結果以下:

       h & (table.length-1)                     hash                             table.length-1

       8 & (15-1)                                 0100                                1110                   =                0100

       9 & (15-1)                                 0101                   &              1110                   =                0100

       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1)                                 0100                   &              1111                   =                0100

       9 & (16-1)                                 0101                   &              1111                   =                0101

  

   從上面的例子中能夠看出:當它們和15-11110的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,89會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲15的時候,hash值會與15-11110)進行,那麼最後一位永遠是0,而0001001101011001101101111101這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!而當數組長度爲16時,即爲2n次方時,2n-1獲得的二進制數的每一個位上的值都爲1,這使得在低位上&時,獲得的和原hash的低位相同,加之hash(int h)方法對keyhashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值纔會被放到數組中的同一個位置上造成鏈表。

   

   因此說,當數組長度爲2n次冪的時候,不一樣的key算得得index相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

   根據上面 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 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

 

 

   2) 讀取:

 

Java代碼 複製代碼  收藏代碼
  1. public V get(Object key) {   
  2.     if (key == null)   
  3.         return getForNullKey();   
  4.     int hash = hash(key.hashCode());   
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];   
  6.         e != null;   
  7.         e = e.next) {   
  8.         Object k;   
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))   
  10.             return e.value;   
  11.     }   
  12.     return null;   
  13. }  
[java] view plain copy
  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.         e != null;  
  7.         e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;  
  11.     }  
  12.     return null;  
  13. }  

 

 

   有了上面存儲時的hash算法做爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中能夠看出:從HashMapget元素時,首先計算keyhashCode,找到數組中對應位置的某一元素,而後經過keyequals方法在對應位置的鏈表中找到須要的元素。

  

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

 

4.    HashMapresizerehash):

   HashMap中的元素愈來愈多的時候,hash衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操做也會出如今ArrayList中,這是一個經常使用的操做,而在HashMap數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是resize

   那麼HashMap何時進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。

 

 

5.    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

   initialCapacityHashMap的最大容量,即爲底層數組的長度。

   loadFactor:負載因子loadFactor定義爲:散列表的實際元素數目(n)/ 散列表的容量(m)

   負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。

   HashMap的實現中,經過threshold字段來判斷HashMap的最大容量:

Java代碼 複製代碼  收藏代碼
  1. threshold = (int)(capacity * loadFactor);  
[java] view plain copy
  1. threshold = (int)(capacity * loadFactor);  

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

 

Java代碼 複製代碼  收藏代碼
  1. if (size++ >= threshold)      
  2.     resize(2 * table.length);    
[java] view plain copy
  1. if (size++ >= threshold)     
  2.     resize(2 * table.length);    

 

6.    Fail-Fast機制:

   咱們知道java.util.HashMap不是線程安全的,所以若是在使用迭代器的過程當中有其餘線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。

   這一策略在源碼中的實現是經過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增長這個值,那麼在迭代器初始化過程當中會將這個值賦給迭代器的expectedModCount

 

Java代碼 複製代碼  收藏代碼
  1. HashIterator() {   
  2.     expectedModCount = modCount;   
  3.     if (size > 0) { // advance to first entry   
  4.     Entry[] t = table;   
  5.     while (index < t.length && (next = t[index++]) == null)   
  6.         ;   
  7.     }   
  8. }  
[java] view plain copy
  1. HashIterator() {  
  2.     expectedModCount = modCount;  
  3.     if (size > 0) { // advance to first entry  
  4.     Entry[] t = table;  
  5.     while (index < t.length && (next = t[index++]) == null)  
  6.         ;  
  7.     }  
  8. }  

 

 

   在迭代過程當中,判斷modCountexpectedModCount是否相等,若是不相等就表示已經有其餘線程修改了Map:

   注意到modCount聲明爲volatile,保證線程之間修改的可見性。

 

Java代碼 複製代碼  收藏代碼
  1. final Entry<K,V> nextEntry() {      
  2.     if (modCount != expectedModCount)      
  3.         throw new ConcurrentModificationException();  
[java] view plain copy
  1. final Entry<K,V> nextEntry() {     
  2.     if (modCount != expectedModCount)     
  3.         throw new ConcurrentModificationException();  

 

   HashMapAPI中指出:

   由全部HashMap類的「collection 視圖方法所返回的迭代器都是快速失敗的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會徹底失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。

   注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。

 

 

轉載自http://blog.csdn.net/xiancaieeee/article/details/7768506

相關文章
相關標籤/搜索