1. 程式人生 > >HashMap深度解析(一)

HashMap深度解析(一)

       本文來自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/16843543,轉載請註明。
       HashMap可以說是Java中最常用的集合類框架之一,是Java語言中非常典型的資料結構,我們總會在不經意間用到它,很大程度上方便了我們日常開發。在很多Java的筆試題中也會被問到,最常見的,“HashMap和HashTable有什麼區別?”,這也不是三言兩語能說清楚的,這種筆試題就是考察你來筆試之前有沒有複習功課,隨便來個快餐式的複習就能給出簡單的答案。
       HashMap計劃寫兩篇文章,一篇是HashMap工作原理,也就是本文,另一篇是多執行緒下的HashMap會引發的問題。這一年文章寫的有點少,工作上很忙,自己業餘時間也做點東西,就把部落格的時間佔用了,以前是力保一週一篇文章,有點給自己任務的意思,搞的自己很累,文章質量也不高,有時候寫技術文章也是需要靈感的,為了舉一個例子可能要絞盡腦汁,為了一段程式碼可能要驗證好多次,現在想通了,有靈感再寫,需要一定的積累,才能把自己瞭解的知識點總結歸納成文章。
       言歸正傳,瞭解HashMap之前,我們需要知道Object類的兩個方法hashCode和equals,我們先來看一下這兩個方法的預設實現:

[java] view plain copy  print?
  1. /** JNI,呼叫底層其它語言實現 */
  2. publicnativeint hashCode();  
  3. /** 默認同==,直接比較物件 */
  4. publicboolean equals(Object obj) {  
  5.     return (this == obj);  
  6. }  
       equals方法我們太熟悉了,我們經常用於字串比較,String類中重寫了equals方法,比較的是字串值,看一下原始碼實現: [java] view plain copy  print?
  1. publicboolean
     equals(Object anObject) {  
  2.     if (this == anObject) {  
  3.         returntrue;  
  4.     }  
  5.     if (anObject instanceof String) {  
  6.         String anotherString = (String) anObject;  
  7.         int n = value.length;  
  8.         if (n == anotherString.value.length) {  
  9.             char v1[] = value;  
  10.             char
     v2[] = anotherString.value;  
  11.             int i = 0;  
  12.             // 逐個判斷字元是否相等
  13.             while (n-- != 0) {  
  14.                 if (v1[i] != v2[i])  
  15.                         returnfalse;  
  16.                 i++;  
  17.             }  
  18.             returntrue;  
  19.         }  
  20.     }  
  21.     returnfalse;  
  22. }  
       重寫equals要滿足幾個條件:
  • 自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。 
  • 對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。 
  • 傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。 
  • 一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 true 或始終返回 false,前提是物件上 equals 比較中所用的資訊沒有被修改。 
  • 對於任何非空引用值 x,x.equals(null) 都應返回 false。 
       Object 類的 equals 方法實現物件上差別可能性最大的相等關係;即,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件時,此方法才返回 true(x == y 具有值 true)。 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。        下面來說說hashCode方法,這個方法我們平時通常是用不到的,它是為雜湊家族的集合類框架(HashMap、HashSet、HashTable)提供服務,hashCode 的常規協定是:
  • 在 Java 應用程式執行期間,在同一物件上多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是物件上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。 
  • 如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。 
  • 以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼在兩個物件中的任一物件上呼叫 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的物件生成不同整數結果可以提高雜湊表的效能。
       當我們看到實現這兩個方法有這麼多要求時,立刻凌亂了,幸好有IDE來幫助我們,Eclipse中可以通過快捷鍵alt+shift+s調出快捷選單,選擇Generate hashCode() and equals(),根據業務需求,勾選需要生成的屬性,確定之後,這兩個方法就生成好了,我們通常需要在JavaBean物件中重寫這兩個方法。        好了,這兩個方法介紹完之後,我們回到HashMap。HashMap是最常用的集合類框架之一,它實現了Map介面,所以儲存的元素也是鍵值對對映的結構,並允許使用null值和null鍵,其內元素是無序的,如果要保證有序,可以使用LinkedHashMap。HashMap是執行緒不安全的,下篇文章會討論。HashMap的類結構如下:        HashMap中我們最長用的就是put(K, V)和get(K)。我們都知道,HashMap的K值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實現,但隨著內部元素的增多,put和get的效率將越來越低,這裡的時間複雜度是O(n),假如有1000個元素,put時需要比較1000次。實際上,HashMap很少會用到equals方法,因為其內通過一個雜湊表管理所有元素,雜湊是通過hash單詞音譯過來的,也可以稱為散列表,雜湊演算法可以快速的存取元素,當我們呼叫put存值時,HashMap首先會呼叫K的hashCode方法,獲取雜湊碼,通過雜湊碼快速找到某個存放位置,這個位置可以被稱之為bucketIndex,通過上面所述hashCode的協定可以知道,如果hashCode不同,equals一定為false,如果hashCode相同,equals不一定為true。所以理論上,hashCode可能存在衝突的情況,有個專業名詞叫碰撞,當碰撞發生時,計算出的bucketIndex也是相同的,這時會取到bucketIndex位置已儲存的元素,最終通過equals來比較,equals方法就是雜湊碼碰撞時才會執行的方法,所以前面說HashMap很少會用到equals。HashMap通過hashCode和equals最終判斷出K是否已存在,如果已存在,則使用新V值替換舊V值,並返回舊V值,如果不存在 ,則存放新的鍵值對<K, V>到bucketIndex位置。文字描述有些亂,通過下面的流程圖來梳理一下整個put過程。
       現在我們知道,執行put方法後,最終HashMap的儲存結構會有這三種情況,情形3是最少發生的,雜湊碼發生碰撞屬於小概率事件。到目前為止,我們瞭解了兩件事:
  • HashMap通過鍵的hashCode來快速的存取元素。
  • 當不同的物件hashCode發生碰撞時,HashMap通過單鏈表來解決,將新元素加入連結串列表頭,通過next指向原有的元素。單鏈表在Java中的實現就是物件的引用(複合)。
       來鑑賞一下HashMap中put方法原始碼: [java] view plain copy  print?
  1. public V put(K key, V value) {  
  2.     // 處理key為null,HashMap允許key和value為null
  3.     if (key == null)  
  4.         return putForNullKey(value);  
  5.     // 得到key的雜湊碼
  6.     int hash = hash(key);  
  7.     // 通過雜湊碼計算出bucketIndex
  8.     int i = indexFor(hash, table.length);  
  9.     // 取出bucketIndex位置上的元素,並迴圈單鏈表,判斷key是否已存在
  10.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  11.         Object k;  
  12.         // 雜湊碼相同並且物件相同時
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             // 新值替換舊值,並返回舊值
  15.             V oldValue = e.value;  
  16.             e.value = value;  
  17.             e.recordAccess(this);  
  18.             return oldValue;  
  19.         }  
  20.     }  
  21.     // key不存在時,加入新元素
  22.     modCount++;  
  23.     addEntry(hash, key, value, i);  
  24.     returnnull;  
  25. }  
       到這裡,我們瞭解了HashMap工作原理的一部分,那還有另一部分,如,載入因子及rehash,HashMap通常的使用規則,多執行緒併發時HashMap存在的問題等等,這些會留在下一章說明。