隨筆 - 100  文章 - 50  trackbacks - 0
          <2025年5月>
          27282930123
          45678910
          11121314151617
          18192021222324
          25262728293031
          1234567

          常用鏈接

          留言簿(3)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          收藏夾

          我收藏的一些文章!

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

           

          1.    HashMap 概述:

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

             可以看出, 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. }  

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

             addEntry(hash, key, value, i) 方法根據計算出的 hash 值,將 key-value 對放在數組 table  i 索引處。 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. }  

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

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

           

          Java代碼 
          1. static   int  hash( int  h) {  
          2.     h ^= (h >>> 20 ) ^ (h >>>  12 );  
          3.     return  h ^ (h >>>  7 ) ^ (h >>>  4 );  
          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) 方法的代碼如下:

           

          Java代碼 
          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 ;  

             這段代碼保證初始化時 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)                                   0100                                   1110                   =                 0100

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

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

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

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

            

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

             

             所以說,當數組長度為 2  n 次冪的時候,不同的 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. }  

           

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

            

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

           

          4.    HashMap  resize  rehash ):

              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 

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

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

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

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

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

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

           

          Java代碼 
          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. }  

           

           

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

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

           

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

           

              HashMap  API 中指出:

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

             注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的并發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException 。因此,編寫依賴于此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用于檢測程序錯誤。

          posted on 2013-04-12 00:13 fly 閱讀(194) 評論(0)  編輯  收藏 所屬分類: java學習
          主站蜘蛛池模板: 临猗县| 凤翔县| 阳谷县| 乐亭县| 牟定县| 连南| 新邵县| 米林县| 尼勒克县| 杭锦后旗| 安徽省| 襄城县| 盐边县| 元阳县| 昌宁县| 阿拉善左旗| 蕲春县| 常德市| 温宿县| 罗定市| 廉江市| 邳州市| 松滋市| 绥德县| 元氏县| 桂林市| 敦化市| 馆陶县| 讷河市| 疏附县| 武强县| 蓬莱市| 周至县| 湟源县| 金山区| 突泉县| 彰武县| 尼勒克县| 海原县| 宜章县| 襄垣县|