日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

深入Java集合學(xué)習(xí)系列:HashMap的實(shí)現(xiàn)原理

 燮羽 2010-12-14

1.    HashMap 概述:

   HashMap 是基于哈希表的 Map 接口的非同步實(shí)現(xiàn)。此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。

 

2.    HashMap 的數(shù)據(jù)結(jié)構(gòu):

    java 編程語言中,最基本的結(jié)構(gòu)就是兩種,一個是數(shù)組,另外一個是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個基本結(jié)構(gòu)來構(gòu)造的, HashMap 也不例外。HashMap 實(shí)際上是一個“鏈表散列”的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體。

  
從上圖中可以看出,
 HashMap 底層就是一個數(shù)組結(jié)構(gòu),數(shù)組中的每一項(xiàng)又是一個鏈表。當(dāng)新建一個 HashMap 的時候,就會初始化一個數(shù)組。

   源碼如下:

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

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

 

3.    HashMap 的存取實(shí)現(xiàn):

   1) 存儲:

Java代碼 
  1. public  V put(K key, V value) {  
  2.     // HashMap允許存放null鍵和null值。   
  3.     // 當(dāng)key為null時,調(diào)用putForNullKey方法,將value放置在數(shù)組第一個位置。   
  4.     if  (key ==  null )  
  5.         return  putForNullKey(value);  
  6.     // 根據(jù)key的keyCode重新計(jì)算hash值。   
  7.     int  hash = hash(key.hashCode());  
  8.     // 搜索指定hash值在對應(yīng)table中的索引。   
  9.     int  i = indexFor(hash, table.length);  
  10.     // 如果 i 索引處的 Entry 不為 null,通過循環(huán)不斷遍歷 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. }  

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

   addEntry(hash, key, value, i) 方法根據(jù)計(jì)算出的 hash 值,將 key-value 對放在數(shù)組 table  i 索引處。 addEntry  HashMap 提供的一個包訪問權(quán)限的方法,代碼如下:

Java代碼 
  1. void  addEntry( int  hash, K key, V value,  int  bucketIndex) {  
  2.     // 獲取指定 bucketIndex 索引處的 Entry    
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     // 將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來的 Entry   
  5.     table[bucketIndex] = new  Entry<K,V>(hash, key, value, e);  
  6.     // 如果 Map 中的 key-value 對的數(shù)量超過了極限   
  7.     if  (size++ >= threshold)  
  8.     // 把 table 對象的長度擴(kuò)充到原來的2倍。   
  9.         resize(2  * table.length);  
  10. }  

   當(dāng)系統(tǒng)決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value ,僅僅只是根據(jù) key 來計(jì)算并決定每個 Entry 的存儲位置。我們完全可以把 Map 集合中的 value 當(dāng)成 key 的附屬,當(dāng)系統(tǒng)決定了 key 的存儲位置之后, value 隨之保存在那里即可。

   hash(int h) 方法根據(jù) key  hashCode 重新計(jì)算一次散列。此算法加入了高位計(jì)算,防止低位不變,高位變化時,造成的 hash 沖突。

 

Java代碼 
  1. static   int  hash( int  h) {  
  2.     h ^= (h >>> 20 ) ^ (h >>>  12 );  
  3.     return  h ^ (h >>>  7 ) ^ (h >>>  4 );  
  4. }  

 

 

   我們可以看到在 HashMap 中要找到某個元素,需要根據(jù) key  hash 值來求得對應(yīng)數(shù)組中的位置。如何計(jì)算這個位置就是 hash 算法。前面說過 HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,所以我們當(dāng)然希望這個 HashMap 里面的 元素位置盡量的分布均勻些,盡量使得每個位置上的元素?cái)?shù)量只有一個,那么當(dāng)我們用 hash 算法求得這個位置的時候,馬上就可以知道對應(yīng)位置的元素就是我們要的,而不用再去遍歷鏈表,這樣就大大優(yōu)化了查詢的效率。

   對于任意給定的對象,只要它的 hashCode() 返回值相同,那么程序調(diào)用 hash(int h) 方法所計(jì)算得到的 hash 碼值總是相同的。我們首先想到的就是把 hash 值對數(shù)組長度取模運(yùn)算,這樣一來,元素的分布相對來說是比較均勻的。但是,    運(yùn)算的消耗還是比較大的,在 HashMap 中是這樣做的:調(diào)用 indexFor(int h, int length) 方法來計(jì)算該對象應(yīng)該保存在 table 數(shù)組的哪個索引處。 indexFor(int h, int length) 方法的代碼如下:

 

Java代碼 
  1. static   int  indexFor( int  h,  int  length) {  
  2.     return  h & (length- 1 );  
  3. }  

 

 

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

Java代碼 
  1. int  capacity =  1 ;  
  2.     while  (capacity < initialCapacity)  
  3.         capacity <<= 1 ;  

   這段代碼保證初始化時 HashMap 的容量總是 2  n 次方,即底層數(shù)組的長度總是為 2  n 次方。

當(dāng) length 總是 2  n 次方時, h& (length-1) 運(yùn)算等價(jià)于對 length 取模,也就是 h%length ,但是 &  % 具有更高的效率。

   這看上去很簡單,其實(shí)比較有玄機(jī)的,我們舉個例子來說明:

   假設(shè)數(shù)組長度分別為 15  16 ,優(yōu)化后的 hash 碼分別為 8  9 ,那么 & 運(yùn)算后的結(jié)果如下:

       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

  

   從上面的例子中可以看出:當(dāng)它們和 15-1  1110     的時候,產(chǎn)生了相同的結(jié)果,也就是說它們會定位到數(shù)組中的同一個位置上去,這就產(chǎn)生了碰撞, 8  9 會被放到數(shù)組中的同一個位置上形成鏈表,那么查詢的時候就需要遍歷這個鏈 表,得到 8 或者 9 ,這樣就降低了查詢的效率。同時,我們也可以發(fā)現(xiàn),當(dāng)數(shù)組長度為 15 的時候, hash 值會與 15-1  1110 )進(jìn)行    ,那么 最后一位永遠(yuǎn)是 0 ,而 0001 , 0011  0101 , 1001  1011 , 0111 , 1101 這幾個位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長度小了很多,這意味著進(jìn)一步增加了碰撞的幾率,減慢了查詢的效率!而當(dāng)數(shù)組長度為 16時,即為 2  n 次方時, 2n -1 得到的二進(jìn)制數(shù)的每個位上的值都為 1 ,這使得在低位上 & 時,得到的和原 hash 的低位相同,加之hash(int h) 方法對 key  hashCode 的進(jìn)一步優(yōu)化,加入了高位計(jì)算,就使得只有相同的 hash 值的兩個值才會被放到數(shù)組中的同一個位置上形成鏈表。

   

   所以說,當(dāng)數(shù)組長度為 2  n 次冪的時候,不同的 key 算得得 index 相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

   根據(jù)上面 put 方法的源代碼可以看出,當(dāng)程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據(jù)該 key  hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry  key  hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry  key 通過equals 比較返回 true ,新添加 Entry  value 將覆蓋集合中原有 Entry  value ,但 key 不會覆蓋。如果這兩個 Entry  key 通過equals 比較返回 false ,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位于 Entry 鏈的頭部 —— 具體說明繼續(xù)看 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. }  

 

   有了上面存儲時的 hash 算法作為基礎(chǔ),理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從 HashMap  get 元素時,首先計(jì)算 key  hashCode ,找到數(shù)組中對應(yīng)位置的某一元素,然后通過 key  equals 方法在對應(yīng)位置的鏈表中找到需要的元素。

  

   3) 歸納起來簡單地說, HashMap 在底層將 key-value 當(dāng)成一個整體進(jìn)行處理,這個整體就是一個 Entry 對象。 HashMap 底層采用一個 Entry[] 數(shù)組來保存所有的 key-value 對,當(dāng)需要存儲一個 Entry 對象時,會根據(jù) hash 算法來決定其在數(shù)組中的存儲位置,在根據(jù)equals 方法決定其在該數(shù)組位置上的鏈表中的存儲位置;當(dāng)需要取出一個 Entry 時,也會根據(jù) hash 算法找到其在數(shù)組中的存儲位置,再根據(jù) equals 方法從該位置上的鏈表中取出該 Entry 

 

4.    HashMap  resize  rehash ):

   當(dāng) HashMap 中的元素越來越多的時候, hash 沖突的幾率也就越來越高,因?yàn)閿?shù)組的長度是固定的。所以為了提高查詢的效率,就要對 HashMap 的數(shù)組進(jìn)行擴(kuò)容,數(shù)組擴(kuò)容這個操作也會出現(xiàn)在 ArrayList 中,這是一個常用的操作,而在 HashMap 數(shù)組擴(kuò)容之后,最消耗性能的點(diǎn)就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計(jì)算其在新數(shù)組中的位置,并放進(jìn)去,這就是 resize 。

   那么 HashMap 什么時候進(jìn)行擴(kuò)容呢?當(dāng) HashMap 中的元素個數(shù)超過數(shù)組大小 *loadFactor 時,就會進(jìn)行數(shù)組擴(kuò)容, loadFactor 的默認(rèn)值為 0.75 ,這是一個折中的取值。也就是說,默認(rèn)情況下,數(shù)組大小為 16 ,那么當(dāng) HashMap 中元素個數(shù)超過 16*0.75=12 的時候,就把數(shù)組的大小擴(kuò)展為 2*16=32 ,即擴(kuò)大一倍,然后重新計(jì)算每個元素在數(shù)組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知 HashMap 中元素的個數(shù),那么預(yù)設(shè)元素的個數(shù)能夠有效的提高 HashMap 的性能。

 

5.    HashMap 的性能參數(shù):

   HashMap 包含如下幾個構(gòu)造器:

   HashMap() :構(gòu)建一個初始容量為 16 ,負(fù)載因子為 0.75  HashMap 。

   HashMap(int initialCapacity) :構(gòu)建一個初始容量為 initialCapacity ,負(fù)載因子為 0.75  HashMap 。

   HashMap(int initialCapacity, float loadFactor) :以指定初始容量、指定的負(fù)載因子創(chuàng)建一個 HashMap 

   HashMap 的基礎(chǔ)構(gòu)造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數(shù),它們是初始容量 initialCapacity 和加載因子loadFactor 。

   initialCapacity  HashMap 的最大容量,即為底層數(shù)組的長度。

   loadFactor :負(fù)載因子 loadFactor 定義為:散列表的實(shí)際元素?cái)?shù)目 (n)/ 散列表的容量 (m) 。

   負(fù)載因子衡量的是一個散列表的空間的使用程度,負(fù)載因子越大表示散列表的裝填程度越高,反之愈小。對于使用鏈表法的散列表來說,查找一個元素的平均時間是 O(1+a) ,因此如果負(fù)載因子越大,對空間的利用更充分,然而后果是查找效率的降低,集中表現(xiàn)就是迭代遍歷會變慢;如果負(fù)載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴(yán)重浪費(fèi)。

   HashMap 的實(shí)現(xiàn)中,通過 threshold 字段來判斷 HashMap 的最大容量:

Java代碼 
  1. threshold = ( int )(capacity * loadFactor);  

   結(jié)合負(fù)載因子的定義公式可知, threshold 就是在此 loadFactor  capacity 對應(yīng)下允許的最大元素?cái)?shù)目,超過這個數(shù)目就重新 resize,以降低實(shí)際的負(fù)載因子。默認(rèn)的的負(fù)載因子 0.75 是對空間和時間效率的一個平衡選擇。當(dāng)容量超出此最大容量時, resize 后的HashMap 容量是容量的兩倍:

 

Java代碼 
  1. if  (size++ >= threshold)     
  2.     resize(2  * table.length);    

 

6.    Fail-Fast 機(jī)制:

   我們知道 java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了 map ,那么將拋出ConcurrentModificationException ,這就是所謂 fail-fast 策略。

   這一策略在源碼中的實(shí)現(xiàn)是通過 modCount 域, modCount 顧名思義就是修改次數(shù),對 HashMap 內(nèi)容的修改都將增加這個值,那么在迭代器初始化過程中會將這個值賦給迭代器的 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. }  

 

 

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

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

 

Java代碼 
  1. final  Entry<K,V> nextEntry() {     
  2.     if  (modCount != expectedModCount)     
  3.         throw   new  ConcurrentModificationException();  

 

    HashMap  API 中指出:

   由所有 HashMap 類的 “collection 視圖方法  所返回的迭代器都是快速失敗的:在迭代器創(chuàng)建之后,如果從結(jié)構(gòu)上對映射進(jìn)行修改,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException 。因此,面對并發(fā)的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時間發(fā)生任意不確定行為的風(fēng)險(xiǎn)。

   注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的并發(fā)修改時,不可能作出任何堅(jiān)決的保證??焖偈〉鞅M最大努力拋出 ConcurrentModificationException 。因此,編寫依賴于此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應(yīng)該僅用于檢測程序錯誤。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多