我的博客java篇-HashMap
概括
HashMap
散列表,通過數組加鏈表的形式構成,在jdk1.8
以后,當鏈表長度大于8
的時候,會轉化成紅黑樹的形式存儲。
允許null
值,同時非有序,非同步(即線程不安全)
put 方法原理
- 根據
key
的hasCode
,通過哈希函數算出存儲位置index
值。 - 如果改位置沒有元素,則直接存儲
- 如果已經有了元素,就判斷該元素的
key
值和key
的hashCode
是否一致,一致就直接覆蓋value
- 如果不一致,就產生了
hash
沖突,就在鏈表上插入新的結點存儲,如果鏈表長度大于8
的話,就轉化成紅黑樹進行存儲 - 插入后,如果數量大于閾值則進行擴容
get 方法原理
- 根據
key
做Hash
映射,得到對應的index
- 遍歷鏈表或者紅黑樹,查找
key
值和key
的hashCode
都相等的節點,返回value
HashMap 的容量為什么一定要是 2 的 n 次方
HashMap
的默認初始長度是16
,并且每次自動擴展或者手動初始化時,長度必須為2
的冪
因為為了讓元素能夠均勻分配,HashMap
需要將key
映射到index
計算方法 index = key的hash值 & (Length - 1)
將key
的hash值
和數組的長度-1
取邏輯與運算
當HashMap
的容量為2
的n
次方的時候,length-1
的值所有二進制為都為1
,這樣能讓index
的計算結果分布更加均勻
擾動函數(hash 函數)
在計算數組的index
值的時候,并不是直接通過key
的hashcode
和lengh-1
取邏輯與運算的,這里有很多文章都是錯誤的講解,需要先將hashcode
通過hash
函數計算出hash
值,在和數組的lengh-1
取邏輯與運算
///jdk1.8源碼
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我們需要hash
值是足夠散列的,這樣才能減少hash
碰撞的概率
任意一個Objec
類型的hashCode
方法得到的hashCode
值是一個int
類型,有32
位。
顯然很少有HashMap
的數組有40
億這么長。如果只是取低幾位的hash
值的話,那么那些低位相同,高位不同的hash
值就碰撞了,如:
/// Hash碰撞示例:
H1: 00000000 00000000 00000000 00000101 & 1111 = 0101
H2: 00000000 11111111 00000000 00000101 & 1111 = 0101
為了解決這類問題,HashMap
想了一種辦法(擾動):將hash
值的高16
位右移并與原hash
值取異或運算^
,混合高16
位和低16
位的值,得到一個更加散列的低16
位的hash
值。如:
00000000 00000000 00000000 00000101 // H1
00000000 00000000 00000000 00000000 // H1 >>> 16
00000000 00000000 00000000 00000101 // hash = H1 ^ (H1 >>> 16) = 5
00000000 11111111 00000000 00000101 // H2
00000000 00000000 00000000 11111111 // H2 >>> 16
00000000 00000000 00000000 11111010 // hash = H2 ^ (H2 >>> 16) = 250
這樣將hashcode
通過擾動函數生成hash
值,就可以減少hash
碰撞的概率了
負載因子
負載因子loadFactor
表示哈希表空間的使用程度,當size
到達數組的容量*
負載因子的時候,會觸發擴容
當負載因子越大,則HashMap
的裝載程度就越高。也就是能容納更多的元素,元素多了,發生hash
碰撞的幾率就會加大,從而鏈表就會拉長,此時的查詢效率就會降低。
當負載因子越小,則鏈表中的數據量就越稀疏,此時會對空間造成浪費,但是此時查詢效率高。
擴容機制
條件: HashMap.Size >= Capacity * LoadFactor
步驟:
1.擴容
創建一個新的Entry
空數組,長度是原數組的 2 倍。
2.ReHash
遍歷原Entry
數組,把所有的Entry
重新Hash
到新數組。為什么要重新Hash
呢?因為長度擴大以后,index
的計算規則也會改變,需要重新計算分布
HashMap 為什么是非線程安全的
當
a
線程準備插入的時候,線程阻塞,b
線程插入成功,a
線程繼續運行的話,會覆蓋b
線程的值ReHash
在并發的情況下可能會形成鏈表環(java8
之前)
頭插法和尾插法
在java8
之前是頭插法,java8
之后是尾插法
使用頭插法,在高并發的場景下會出現鏈表成環的問題
使用頭插會改變鏈表的上的順序,但是如果使用尾插,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了
為啥我們重寫 equals 方法的時候需要重寫 hashCode 方法呢
如何重寫了equals
方法,會導致原先不相同的兩個對象,現在相同了,但是他們的hashcode
還是不相同,違背了相同的對象返回相同的 hash 值,不同的對象返回不同的 hash 值的設計初衷
使用HashMap時,謹慎使用可變對象作為key鍵
如果key
對象是可變的,那么key
的哈希值就可能改變。在HashMap
中可變對象作為Key
會造成數據丟失。因為我們再進行hash & (length - 1)
取模運算計算位置查找對應元素時,位置可能已經發生改變,導致數據丟失
最好選擇不可變對象作為key
。例如String
,Integer
等不可變類型作為key
是非常明智的
HashMap 和 HashTable 的區別
- HashMap 的 key 和 value 都允許為 null,Hashtable 的 key 和 value 都不允許為 null
- HashMap 的初始化容量為 16,并且容器容量一定是 2 的 n 次方,擴容時,是以原容量 2 倍 的方式 進行擴容。Hashtable 初始化容量為 11 擴容時,是以原容量 2 倍 再加 1 的方式進行擴容。即
int newCapacity = (oldCapacity << 1) + 1
- 散列分布方式
-
HashMap
是先將key
鍵的hashCode
經過擾動函數擾動后得到hash
值,然后再利用hash & (length - 1)
的方式代替取模,得到元素的存儲位置 -
Hashtable
則是除留余數法進行計算存儲位置的(因為其默認容量也不是2
的n
次方。所以也無法用位運算替代模運算),int index = (hash & 0x7FFFFFFF) % tab.length
-
- 線程安全
-
HashMap
不是線程安全,如果想線程安全,可以通過調用synchronizedMap(Map m)
使其線程安全。但是使用時的運行效率會下降,所以建議使用ConcurrentHashMap
容器以此達到線程安全。 -
Hashtable
則是線程安全的,每個操作方法前都有synchronized
修飾使其同步,但運行效率也不高,所以還是建議使用ConcurrentHashMap
容器以此達到線程安全。
-
Hashtable
是一個遺留容器,如果我們不需要線程同步,則建議使用HashMap
,如果需要線程同步,則建議使用ConcurrentHashMap