HashMap的實現原理

在很多應用中,都要用到一種動態集合結構。例如,計算機程序設計語言的編譯程序需要維護一個符號表,其中元素的關鍵字值爲任意字符串,與語言中的標示符對應。實現字典的一種有效數據結構爲哈希表(hashmap)。在最壞情況下,在哈希表中,查找一個元素的時間與在鏈表中查找一個元素的時間相同,在最壞情況下都O(n),但在實踐中,哈希技術的效率是很高的。在一些合理的假設下,在哈希表中查找一個元素的期望時間爲O(1)。

1. HashMap實現原理(Java)

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

1.1  HashMap的數據結構

在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。如圖1-1所示。


圖1-1 哈希表數據結構

從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。如圖1-2。


圖1-2 構建哈希表的java源碼

上面的Entry就是數組中的元素,它持有一個指向下一個元素的引用,這就構成了鏈表。

當我們往hashmap中put元素的時候,先根據key的hash值得到這個元素在數組中的位置(即下標),然後就可以把這個元素放到對應的位置中了。如果這個元素所在的位子上已經存放有其他元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。從hashmap 中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

1.2  hash算法

我們可以看到在hashmap中要找到某個元素,需要根據key的hash值來求得 對應數組中的位置。如何計算這個位置就是hash算法。前面說過hashmap的數據結構是數組和鏈表的結合,所以我們當然希望這個hashmap裏面的 元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們 要的,而不用再去遍歷鏈表。

所以我們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式那?java中時這樣做的,如圖1-3所示。


圖1-3 hash算法

首先算得key得hashcode值,然後跟數組的長度-1做一次“與”運算 (&)。看上去很簡單,其實比較有玄機。比如數組的長度是2的4次方,那麼hashcode就會和2的4次方-1做“與”運算。很多人都有這個疑 問,爲什麼hashmap的數組初始化大小都是2的次方大小時,hashmap的效率最高,我以2的4次方舉例,來解釋一下爲什麼數組大小爲2的冪時 hashmap訪問的性能最高。

  如圖1-4所示,左邊兩組是數組長度爲16(2的4次方),右邊兩組是數組長度爲15。兩組的hashcode均爲8和9,但是很明顯,當它們和1110“與”的 時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那麼查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度爲15的時候,hashcode的值會與14(1110)進行“與”,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的機率,減慢了查詢的效率!


圖1-4hash算法評測圖

所以說,當數組長度爲2的n次冪的時候,不同的key算得得index相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。說到這裏,我們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼可以得知是16,爲什麼是16,而不是15,也不是20呢,看到上面 annegu的解釋之後我們就清楚了吧,顯然是因爲16是2的整數次冪的原因,在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。所以,在存儲大容量數據的時候,最好預先指定hashmap的size爲2的整數次冪次方。就算不指定的話,也會以大於且最接近指定值大小的2次冪來初始化的。

1.3    HashMap的存取實現

PUT:


圖1-5 hashmap的存儲實現

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

 addEntry(hash, key, value, i)方法根據計算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如圖1-6所示:


圖1-6 addEntry方法

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

 hash(int h)方法根據key的hashCode重新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。源碼如圖1-7所示。


圖1-7所示 hash函數源碼

GET:


圖1-8 hash讀取

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

1.4    HashMap的性能參數

HashMap 包含如下幾個構造器:

HashMap():構建一個初始容量爲 16,負載因子爲0.75 的 HashMap。

HashMap(int initialCapacity):構建一個初始容量爲 initialCapacity,負載因子爲 0.75 的 HashMap。

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

HashMap的基礎構造器HashMap(int initialCapacity, floatloadFactor)帶有兩個參數,它們是初始容量initialCapacity和加載因子loadFactor。

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

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

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

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

threshold = (int)(capacity *loadFactor);

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

 


發佈了14 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章