1. 程式人生 > >HashMap的實現原理總結

HashMap的實現原理總結

1.簡介

   HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序。

2.HashMap的資料結構:

HashMap實際上是一個“連結串列的陣列”的資料結構,每個元素存放連結串列頭結點的陣列,即陣列和連結串列的結合體

     

從上圖中可以看出,HashMap底層就是一個數組結構,陣列中的每一項又是一個連結串列。當新建一個HashMap的時候,就會初始化一個數組。原始碼如下:

      

可以看出,Entry就是陣列中的元素,每個 Map.Entry 其實就是一個key-value,它持有一個指向下一個元素的引用,這就構成了連結串列。

3.HashMap的存取實現:

  1) 儲存:

     

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

  addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在陣列table的 i 索引處。addEntry 是HashMap 提供的一個包訪問許可權

的方法(就是沒有public,protected,private這三個訪問許可權修飾詞修飾,為預設的訪問許可權,用default表示,但在程式碼中沒有這個default),程式碼如下:

      

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

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

     

      我們可以看到在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) 方法的程式碼如下:

     

     這個方法非常巧妙,它通過 h & (table.length -1) 來得到該物件的儲存位,而HashMap底層陣列的長度總是 2 次方,這是HashMap在速度上的優化。 HashMap 構造器中有如下程式碼:

      

     這段程式碼保證初始化時HashMap的容量總是2的n次方,即底層陣列的長度總是為2的n次方。

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

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

  假設陣列長度分別為15和16,優化後的hash碼分別為8和9,那麼&運算後的結果如下:

     

    從上面的例子中可以看出:當8、9兩個數和(15-1)2=(1110)進行“與運算&”的時候,產生了相同的結果,都為0100,也就是說它們會定位到陣列中的同一個位置上去,這就產生了碰撞,8和9會被放到陣列中的同一個位置上形成連結串列,那麼查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當陣列長度為15的時候,hash值會與(15-1)2=(1110)進行“與運算&”,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,陣列可以使用的位置比陣列長度小了很多,這意味著進一步增加了碰撞的機率,減慢了查詢的效率!

  而當陣列長度為16時,即為2的n次方時,2n-1得到的二進位制數的每個位上的值都為1(比如(24-1)2=1111),這使得在低位上&時,得到的和原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() 方法的說明。

      

     有了上面儲存時的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(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

      HashMap擴容的程式碼如下所示:

      

5.HashMap的效能引數:

    HashMap 包含如下幾個構造器:

  1.    HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
  2.    HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
  3.    HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。
  4.    HashMap的基礎構造器HashMap(int initialCapacity, float loadFactor)帶有兩個引數,它們是初始容量initialCapacity和載入因子loadFactor。
  5.    initialCapacity:HashMap的最大容量,即為底層陣列的長度。
  6.    loadFactor:負載因子loadFactor定義為:散列表的實際元素數目(n)/ 散列表的容量(m)。

  負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小對於使用連結串列法的散列表來說,查詢一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查詢效率的降低;如果負載因子太小,那麼散列表的資料將過於稀疏,對空間造成嚴重浪費。

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

     

      結合負載因子的定義公式可知,threshold就是在此loadFactorcapacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子(也就是說雖然陣列長度是capacity,但其擴容的臨界值確是threshold)。預設的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是容量的兩倍:

      

6.Fail-Fast機制:

  我們知道java.util.HashMap不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了map,那麼將丟擲ConcurrentModificationException,這就是所謂fail-fast策略。這個在core java這本書中也有提到。)

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

          

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

  注意到modCount宣告為volatile,保證執行緒之間修改的可見性。(volatile之所以執行緒安全是因為被volatile修飾的變數不儲存快取,直接在記憶體中修改,因此能夠保證執行緒之間修改的可見性)

      

     在HashMap的API中指出:

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

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



相關推薦

HashMap實現原理總結

1.簡介    HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序。 2.HashMap的資料結構: HashMap實際上是一個“連結串列的陣列”的資料結構,每個元素存放連結串列頭結點

jdk7中hashmap實現原理和jdk8中hashmap的改進方法總結

原文連結:http://blog.csdn.net/vking_wang/article/details/141665931. HashMap的資料結構資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。      陣列陣列儲存區間是連續的,佔用記憶體嚴重

HashMap實現原理

一個 ash img 方法 shm 步長 初始 2的n次冪 http HashMap的數據結構是數組+單向鏈表,數組裏面存儲就是鏈表的Head節點,鏈表節點存儲的是我們put進去的key/value。 如果要實現HashMap,主要有三個重要的功能點: 1.初

1.Java集合-HashMap實現原理及源碼分析

int -1 詳細 鏈接 理解 dac hash函數 順序存儲結構 對象儲存   哈希表(Hash Table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實

探索HashMap實現原理及其在jdk8數據結構的改進

重點 his 說了 比較 filter new exist adf 網絡 因為網上已經太多的關於HashMap的相關文章了,為了避免大量重復,又由於網上關於java8的HashMap的相關文章比較少,至少我沒有找到比較詳細的。所以才有了本文。 本文主要的內容: 1.Ha

Synchronized實現原理總結

依次 mark adding ali zed 線程id 遍歷 安全 ren synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。對於普通同步方法,鎖是當前實例對象。對於靜態同步方法,鎖是當前類的Class對象。對於同步方法塊,

hashmap實現原理2

ace 數據 取數 tool 數組存儲 同時 個數 array jsb put和get都首先會調用hashcode方法,去查找相關的key,當有沖突時,再調用equals(這也是為什麽剛開始就重溫hashcode和equals的原因)! HashMap基於hashing原

HashMap實現原理及源碼分析

響應 應用場景 取模運算 圖片 mat 直接 maximum 計算 時間復雜度 哈希表(hash table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也

HashMap實現原理和源碼分析

aci 鍵值對 creat 變化 遍歷數組 沖突的解決 查看 seed 二分 作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>原文:https://www.cnblogs.com/cheng

轉:HashMap實現原理分析(面試問題:兩個hashcode相同 的對象怎麽存入hashmap的)

影響 strong 就會 怎麽 ash 地方 shm nbsp 擔心 原文地址:https://www.cnblogs.com/faunjoe88/p/7992319.html 主要內容: 1)put 疑問:如果兩個key通過hash%Entry[].length得到的

ThreadLocal 實現原理總結

ThreadLocal 用於在不同執行緒中互不干擾的儲存並提供資料。 這裡不對原始碼進行深究,只淺顯的對實現原理進行了解。 本次涉及到的原始碼為 Source for Android 27. ThreadLocal 的實現,需要藉助到 ThreadLocalMap。

HashMap實現原理(部分原始碼)

JAVA裡面有HashMap、Hashtable、HashSet三種hash集合的實現原始碼,這裡總結下,理解錯誤的地方還望指正! HashMap和Hashtable的區別 1、兩者最主要的區別在於Hashtable是執行緒安全,而HashMap則非執行緒安全。 Hashtabl

HashMap實現原理及原始碼分析(轉載)

作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>        雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,

HashMap實現原理分析及簡單實現一個HashMap

HashMap實現原理分析及簡單實現一個HashMap 歡迎關注作者部落格 簡書傳送門 轉載@原文地址   HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程式設計師都知道HashMap,都知道哪裡要用HashMap,知道HashMap和

HashMap實現原理(jdk1.7/jdk1.8)

HashMap的底層實現:  1、簡單回答    JDK1.7:HashMap的底層實現是:陣列+連結串列  JDK1.8:HashMap的底層實現是:陣列+連結串列/紅黑樹     為什麼要紅黑樹?  紅黑樹:一個自平衡的二

MyBatis快取實現原理總結概述

MyBatis提供兩種快取:一級快取【預設開啟的SqlSession級別的快取】和二級快取【Namespace Mapper級別的快取】。 1. SqlSession快取 存在SESSION【預設】和STATEMENT兩個選項,SESSION在MyBatis會話中執行的所有

HashMap實現原理及原始碼分析

雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,許多快取技術(比如memcached)的核心其實就是在記憶體中維護一張大的雜湊表,而HashMap的實現原理也常常出現在各類的面試題中,重要性可見一斑。本文會對java集合框架中的對

hashmap實現原理(雜湊值計算,put方法,擴容) jdk1.8帶來的優化 hashmap併發安全 ConcurrentHashMap

HashMap的原始碼,實現原理,JDK8中對HashMap做了怎樣的優化。 ArrayList和LinkedList的優缺點——陣列的特點是:定址容易,插入和刪除困難;而連結串列的特點是:定址困難,插入和刪除容易。 hashmap底層

hash演算法 (hashmap 實現原理)

Hash ,一般翻譯做“ 雜湊” ,也有直接音譯為“ 雜湊” 的,就是把任意長度的輸入(又叫做預對映, pre-image),通過雜湊演算法,變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的

從原始碼分析HashMap實現原理

HashMap 基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。另外,HashMap是非