概述
本篇文章我們來聊聊大家日常開發中常用的一個集合類 - HashMap。HashMap 最早出現在 JDK 1.2中,底層基於散列算法實現。HashMap 允許 null 鍵和 null 值,在計算哈鍵的哈希值時,null 鍵哈希值為 0。HashMap 並不保證鍵值對的順序,這意味着在進行某些操作后,鍵值對的順序可能會發生變化。另外,需要注意的是,HashMap 是非線程安全類,在多線程環境下可能會存在問題。
屬性詳解
DEFAULT_INITIAL_CAPACITY 默認初始容量
MAXIMUM_CAPACITY 最大容量
DEFAULT_LOAD_FACTOR 默認負載因子
TREEIFY_THRESHOLD 一個桶的樹化閾值(超過此值會變成紅黑樹)
UNTREEIFY_THRESHOLD 一個樹的鏈表還原閾值(小於此值在resize的時候會變回鏈表)
MIN_TREEIFY_CAPACITY 哈希表的最小樹形化容量(為了避免進行擴容、樹形化選擇的沖突,這個值不能小於 4 * TREEIFY_THRESHOLD)
table
HashMap中的數組(hash表)。hash表的長度總是在2^n。至於原因嗎,后面專門會說的。數組里存儲的是Node節點的數據
entrySet
Node<K,V> 節點構成的 set
size
當前map中存儲節點的數據
modCount
hashMap發生結構性變化的次數,節點轉紅黑樹、擴容等操作。
threshold、loadFactor
擴容闕值和裝載因子。
源碼知識點必備
getGenericInterfaces和getInterfaces區別
getGenericInterfaces獲取直接接口
getInterfaces獲取所有接口
ParameterizedType
是Type的子接口,表示一個有參數的類型。就是我們俗稱的泛型。實現這個接口的類必須提供equals方法。
getRawType
返回最外層<>前面那個類型,即Map<K ,V>的Map。
getActualTypeArguments
獲取“泛型實例”中<>里面的“泛型變量”(也叫類型參數)的值,這個值是一個類型。因為可能有多個“泛型變量”(如:Map<K,V>),所以返回的是一個Type[]。
注意:無論<>中有幾層<>嵌套,這個方法僅僅脫去最外層的<>,之后剩下的內容就作為這個方法的返回值,所以其返回值類型是不確定的。
getOwnerType
獲得這個類型的所有者的類型,主要對嵌套定義的內部類而言。列如對java.util.Map.Node<K,V> 調用getOwnerType方法返回的是interface java.util.Map接口
comparableClassFor
HashMap類中有一個comparableClassFor(Object x)方法,當x的類型為X,且X直接實現了Comparable接口(比較類型必須為X類本身)時,返回x的運行時類型;否則返回null。通過這個方法,我們可以搞清楚一些與類型、泛型相關的概念和方法
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
hashCode與自己的高16為進行異或 。 這樣更分散
ps:
& : 全部為1則為1,否則為0 偏0
| : 有一個為1則為1,否則為0 偏1
^ : 相同為0 不同為1 更加均衡。 均勻(分散)
hash表維護
在文章開頭我們就解釋了HashMap中table就是我們的hash表。直觀上我們可以理解成一個開辟空間的數組。HashMap通過hash(key)這個方法獲取hash值。然后通過hash值確定key在hash表中的位置((n - 1) & hash
)。
綜合上圖我們也會發現問題了。key的個數是無限的。但是我們的hash表是有限的。如何能保證hash(key)不會落在同一個位置呢。答案是不能。換句話說就是我們hash(key)無法保證。也就是hashMap會發生hash碰撞的。hash函數只能盡量避免hash碰撞。上面的(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
就是為了讓hash更加分散點。這一點上面也作出了解釋。
HashMap 數組長度是2^n ?
上面解釋了hashmap中hash函數為什么要^ 。 那么深度源碼的小伙伴可能會問,為什么hashmap默認容量是16以及后期每次擴容的時候為什么是翻倍擴容。簡而言之。為什么hashMap數組長度永遠是2的倍數呢。
上面我們知道如何通過hash確定在數組中位置的。
(n - 1) & hash
關於這個n是數組的長度,hash就是key值通過hash函數計算出來的hash值。
& 運算規則是: 全部為1則為1,否則為0
假設目前hashMap容量是16 , 我們來看看在擴容前后我們key的在是數組中的索引。
經過圖片鮮明的對比我們發現,擴容前后是不會影響原來數據(高位為0)的索引位置的。這里要注意的是並不是說所有數據不受影響,只要原來從右至左第五位為0的hash會受影響,其他不會。這樣大大減少了數組位置調換的操作。性能上也大大的提高了。從這里也可以看出hashmap容量越大,擴容是越復雜,因為容量越大,需要換位置的索引越多。
那么如果我們擴容是不是選擇擴大2倍 , 我們看看會發生什么樣情況。
上圖中是有16擴展成了24容量。這個時候我們會發現除了(從右至左)第五位以為第四位的數據也發生了變化。這樣造成的接口是第四位和第五位的數據都會變化。這樣增加了索引位置的數量。所以我們需要在每次擴容為原來的2倍。
神奇的hashmap遍歷
做Java的肯定會遇到的一種情況是,為什么我的map遍歷的順序和我添加的順序不一致呢。有時候我們做列表展示的時候對順序是有要求的。但是hashmap偏偏和我們想的不一樣。今天華仔帶你看看為什么會出現這種神奇的遍歷。
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
從上面的代碼我們可以看出來hashmap在遍歷時候,是先遍歷數組然后取到數組中鏈表(紅黑樹)按照順序獲取node節點的。也即是說我們先按數組再按鏈表順序。而不是按照你添加先后的順序。而上面我們了解添加的node決定其位置的是key的hash值。所以這就解釋了為什么hashmap遍歷的時候和我們添加不一致的了。
put 流程跟蹤
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其他方法原理是相同的。值得注意的是remove后臨界情況會發生紅黑樹轉鏈表。所以轉紅黑樹的這個闕值的選取有時候會影響性能的高低。下面看看put的實際源碼吧。拜讀下大佬的代碼。
上面的代碼可以看出來put實際調用的方法是putVal();
int hash : key對應的hash值
K key, : key
V value, : value
onlyIfAbsent : 如果存在則忽略,默認false表示新值會覆蓋舊值
boolean evict: 表示是否在構造table時調用
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
寒暄一句
個人幾天時間總結的,有網上前輩的總結,也有加入個人的想法。
再次申明:以上圖片部分來自網絡。