前言
本文是基於Java 8
的HashMap
進行分析,主要是分析HashMap
中的put()
和get()
方法。
下面將會分析這部分的源碼,如果覺得源碼分析內容太囉嗦,可以跳過源碼部分,直接看源碼下面的總結。
put()方法源碼分析
HashMap
的put()
方法是我們最常用的方法,但是put()
方法是怎麼工作的呢?
put()方法
/**
* HashMap的put()方法支持key/value爲null
*/
public V put(K key, V value) {
//實際上是先調用HashMap的hash()方法獲取到key的hash值
//然後調用HashMap的putVal()方法
return putVal(hash(key), key, value, false, true);
}
put()
方法實際上是
- 調用
hash()
方法獲取到key
的hash
值 - 調用
putVal()
方法存儲key-value
核心方法是putVal()
方法,下面我會先分析一下hash()
方法,因爲這個方法涉及到hash
值這個關鍵屬性的計算。
hash()方法
static final int hash(Object key) {
int h;
// key爲null時,hash值爲0
// key不爲null時,調用key對象的hashCode()方法並通過位運算異或和無符號右移將高位分散到低位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
hash()
方法指定了null
的hash
值爲0
。這樣就可以支持key
爲null
。 -
(h = key.hashCode()) ^ (h >>> 16)
這段代碼通過位運算異或和無符號右移將高位分散到低位,這樣做可以減少哈希碰撞的概率(這塊不是很清楚原理,是從方法註釋上瞭解到的)
putVal()方法
/**
* Map.put()方法的實際實現
*
* @param hash key的hash值
* @param key 鍵值對中的key
* @param value 鍵值對中的value
* @param onlyIfAbsent 如果爲true,則鍵值對中的值已經存在則不修改這個值
* @param evict 如果爲false,則是處於創建模式
* @return 上一次的value,如果上一次的value不存在,則爲null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//tab用於暫存散列表table。p爲散列表中對應索引的鏈表的頭節點的指針。n存儲tab的長度。i則爲命中的散列表的索引
Node<K,V>[] tab; Node<K,V> p; int n, i;
//給tab和n賦值
//當tab爲null或者tab的長度n爲0時,觸發resize()來初始化tab
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//使用(n - 1) & hash(等價於hash%n)計算命中的散列表索引,同時判斷散列表對應索引的鏈表是否存在
if ((p = tab[i = (n - 1) & hash]) == null)
//散列表對應索引的鏈表不存在則創建一個新的鏈表
tab[i] = newNode(hash, key, value, null);
else {//散列表對應索引的鏈表已存在
Node<K,V> e; K k;
// 判斷頭節點的hash值和key是否與入參的hash值和key一致。需要注意,null的hash值爲0
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 對應的鍵值對已經存在,記錄下來
e = p;
else if (p instanceof TreeNode)//判斷對應的鏈表是否轉化爲紅黑樹
//若是,則直接調用紅黑樹的putTreeVal()方法
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開始,所以爲閾值-1
// 將鏈表轉化爲紅黑樹
treeifyBin(tab, hash);
// 中斷循環
break;
}
// 判斷當前遍歷的節點的hash值和key是否與入參的hash值和key一致,即key是否已經存在
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// key已經存在,中斷循環
break;
// 記錄當前遍歷的節點
p = e;
}
}
if (e != null) { // Map中存在重複的key
V oldValue = e.value;//記錄下舊值
if (!onlyIfAbsent || oldValue == null)//判斷值存在是否可以進行修改以及舊值是否爲null
e.value = value;//修改該節點的值
afterNodeAccess(e);// 鏈表節點的回調方法,此處爲空方法
return oldValue;//返回舊值
}
}
// HashMap發生結構變化,變化次數累加
++modCount;
// 鍵值對個數自增,同時判斷是否達到擴容的閾值
if (++size > threshold)
resize();
// 鏈表節點的回調方法,此處爲空方法
afterNodeInsertion(evict);
// 此處返回null是因爲鏈表新增了節點,所以上一次的值必然爲null
return null;
}
putVal()
方法的關鍵點:
- 若
table
沒有初始化則調用reszie()
方法初始化。 - 計算命中的散列表索引位置,公式爲
(n - 1) & hash
(等價於hash%n
)。其中n
爲散列表長度,hash
爲插入的鍵值對的key
的哈希值。 - 判斷散列表對應索引中的首節點是否爲
null
,若爲null
,則創建鏈表,否則進入下一步。 - 判斷該首節點是否與插入的鍵值對的
key
和hash
一致,若一致則替換該節點的值爲value
,否則進入下一步 - 判斷首節點是否爲樹節點,若是則調用樹節點的
putTreeVal()
方法遍歷紅黑樹,否則遍歷鏈表。 - 遍歷紅黑樹時,若存在
key
和hash
相同的節點就替換對應節點的值value
,若不存在則插入新的樹節點。 - 遍歷鏈表時,若存在
key
和hash
相同的節點就替換對應節點的值爲value
。若找不到key
和hash
相同的節點,則鏈表尾部插入節點,同時進入下一步。 - 若當前鏈表長度大於或等於樹化閾值
TREEIFY_THRESHOLD(8)
時,則將鏈表轉化爲紅黑樹。
get()方法源碼分析
除了HashMap
的put()
方法外,get()
方法也是一個我們常用的方法,下面開始分析其關鍵的源碼。
get()方法
/**
* 返回key對應的value,如果不存在則返回null
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get()
方法實際上是
- 調用
hash()
方法獲取到key
的hash
值 - 調用
getNode()
方法通過key
和hash
獲取對應的value
,不存在則返回null
。
核心方法是getNode()
方法,下面我會先分析一下getNode()
方法。
getNode()方法
/**
* Map.get()方法的實際實現
* @param hash key的哈希值
* @param key 查詢用的key
* @return 節點或者是節點不存在是返回null
*/
final Node<K,V> getNode(int hash, Object key) {
//tab用於暫存散列表table。first爲散列表中對應索引的鏈表的頭節點的指針。n存儲tab的長度。i則爲命中的散列表的索引
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//初始化方法內的變量,同時嘗試命中散列表
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))// 總是先檢查鏈表的頭節點
return first;//頭節點符合直接返回頭節點
if ((e = first.next) != null) {//是否只有一個節點
if (first instanceof TreeNode)//判斷頭節點是否爲紅黑樹節點
return ((TreeNode<K,V>)first).getTreeNode(hash, key);//改爲遍歷紅黑樹
do {//遍歷鏈表是否有符合的節點
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//不存在對應的key,返回null
return null;
}
getNode()
方法的關鍵點:
- 若散列表
table
不爲null
且長度
大於0
且其索引爲(n - 1) & hash
(等價於hash%n
)的節點不爲null
。其中n
爲散列表長度,hash
爲插入的鍵值對的key
的哈希值。則進入下一步,否則直接返回null
- 判斷首節點的
key
和hash
是否與入參一致,若相同則返回首節點,否則進入下一步。 - 判斷節點個數只有
1
個,若是則返回null
,否則進入下一步 - 判斷首節點是否爲樹節點,若是則遍歷紅黑樹,否則爲鏈表,進入下一步
- 遍歷鏈表,檢索
key
和hash
與入參相同的節點,若找到則返回該節點,否則返回null
總結
put()
和get()
方法是HashMap
的常用方法,通過學習其源碼瞭解到HashMap
是如何使用拉鍊法解決哈希衝突。而下面將會通過兩幅圖展示put()
和get()
的執行過程:
-
put()
方法圖解
-
get()
方法圖解