什麼是HashMap?
要想知道什麼是HashMap就要先了解數組和鏈表。數組:查詢速度快,可以根據索引查詢;但插入和刪除比較困難;鏈表:查詢速度慢,需要遍歷整個鏈表,但插入和刪除操作比較容易。HashMap是數組和鏈表組成的,數據結構中又叫“鏈表散列”。HashMap是Java中集合的一部分。它提供了Java的Map接口的基本實現。它將數據存儲在(Key,Value)對中。要訪問一個值,你必須知道它的密鑰,否則,你不能訪問它。
HashMap的過程
- 通過較爲複雜的方法把數據均勻散列到數組中,並在數組中記錄鏈表(或者紅黑樹)的地址。
- 通過一定的方法把數據有規律的存儲在鏈表(或者紅黑樹)中。
HashMap的過程會出現的問題
(這裏需要補充一下——數據傾斜:數組中的一個數組鏈表不斷插入數據,超過他的最大值)
- 如果出現數據傾斜,有可能是因爲選擇的散列方法不合適,需要更改合適的散列方法。當數組散列出現數據傾斜時(都去一個地方了)會發生紅黑樹深度特別大的時候需要改變散列算法。(這不是重哈希)
-
如果hashmap使用的是鏈表並且鏈表的最大值當數據超過最大值時重新申請數據鏈表重新散列。
-
如果hashmap使用的是紅黑樹並且當數據總量超過75%時在進行擴容,重新進行數據散列,重新構建紅黑樹分配數據。
-
既然紅黑樹那麼好,爲啥hashmap不直接採用紅黑樹,而是當大於8個的時候才轉換紅黑樹?如果元素小於8個,查詢成本高,新增成本低,如果元素大於8個,查詢成本低,新增成本高。
-
hashmap的線程不安全。
-
map一對一的應對關係:不只有一個數字肯定還有對應的一個內存地址。
HashMap的時間複雜度
在理想的情況下hashmap的時間複雜度爲O(1),當鏈表換成紅黑樹時hashmap查找的時間複雜依然是O(1),插入和讀取的時候對鏈表進行遍歷(一般遍歷的 時間複雜度爲O(n))但在這並不是這樣,因爲鏈表長度固定所以hashmap的時間複雜度爲常數即O(1)。
HashMap流程圖
HashMap結構示意圖
HashMap 內部結構:可以看作是數組和鏈表結合組成的複合結構,數組被分爲一個個桶,每個桶存儲有一個或多個Entry對象,每個Entry對象包含三部分key、value,next(指向下一個Entry),通過哈希值決定了Entry對象在這個數組的尋址;哈希值相同的Entry對象(鍵值對),則以鏈表形式存儲。當鏈表超過8時,換成紅黑樹結構。
比較hashCode和equals的區別
hashCode是用於查找使用的,而equals是用於比較兩個對象的是否相等的。
HashMap的初始值還要考慮加載因子:(這是用的別人的)
· 哈希衝突:若干Key的哈希值按數組大小取模後,如果落在同一個數組下標上,將組成一條Entry鏈,對Key的查找需要遍歷Entry鏈上的每個元素執行equals()比較。
· 加載因子:爲了降低哈希衝突的概率,默認當HashMap中的鍵值對達到數組大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的數組大小。
· 空間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希衝突的概率。
源碼:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//把 oldTab 中的節點 reHash 到 newTab 中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//若節點是單個節點,直接在 newTab 中進行重定位
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//若是鏈表,進行鏈表的 rehash 操作
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//根據算法 e.hash & oldCap 判斷節點位置 rehash 後是否發生改變
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// rehash 後節點新的位置一定爲原來基礎上加上 oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
總結:
HashMap是線程不安全的,多線程環境下數據可能會發生錯亂,一定要謹慎使用。HashMap的線程不安全遠遠不是數據髒讀這麼簡單,它還有可能會發生死鎖,造成內存飆升100%的問題。