【JDK源碼分析系列】HashMap 源碼分析
【0】HashMap 整體架構
【1】HashMap 新增數據流程
1. 空數組有無初始化,沒有的話初始化
2. 如果通過 key 的 hash 能夠直接找到值, 跳轉到 6,否則到 3
3. 如果 hash 衝突,兩種解決方案:鏈表 or 紅黑樹
4. 如果是鏈表,遞歸循環,把新元素追加到隊尾
5. 如果是紅黑樹,調用紅黑樹新增的方法
6. 通過 2、4、5 將新元素追加成功,再根據 onlyIfAbsent 判斷是否需要覆蓋
7. 判斷是否需要擴容,需要擴容進行擴容,結束
HashMap 代碼
public V put(K key, V value) {
/**
* 四個參數,
* 第一個 : hash值
* 第四個 : 表示如果該key存在值,且爲null的話,則插入新的value,
* 第五個 : 在hashMap中沒有用,可以不用管,使用默認的即可
*/
return putVal(hash(key), key, value, false, true);
}
//1:空數組初始化
//2:key計算的數組索引下,如果沒有值,直接新增賦值
//3:如果hash衝突,分成2種,一個是鏈表,一個是紅黑樹
//4:如果當前桶已經是紅黑樹了,調用紅黑樹新增的方法
//5:如果是鏈表,遞歸循環
//6:鏈表中的元素的key有和入參key相等的,允許覆蓋值的話直接覆蓋
//put方法默認覆蓋
//7:如果新增的元素在鏈表中不存在,則新增,新增到鏈表的尾部
//8:新增時,判斷如果鏈表的長度大於等於8時,轉紅黑樹
//9:如果數組的實際使用大小大於等於擴容的門檻,直接擴容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// n 表示數組的長度, i 爲數組索引下標, p 爲 i 下標位置的 Node 值
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果數組爲空,初始化
// 如果數組爲空, 使用 resize 方法初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// hashCode的算法先右移16 在並上數組大小-1
// 如果當前索引位置是空的,直接生成新的節點在當前索引位置上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果當前索引位置有值的處理方法, 即解決 hash 衝突
else {
// e 當前節點的臨時變量
Node<K,V> e; K k;
// 若節點的 hash key key 的值都相等則直接把當前下標位置的 Node 值賦值給臨時變量
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) {
//如果是最後一個,還找不到和新增的元素相等的,直接新增
//節點是新增到鏈表最後的
// e = p.next 表示從頭開始, 遍歷鏈表
// p.next == null 表明 p 是鏈表的尾節點
if ((e = p.next) == null) {
//p.next是新增的節點,但是e仍然是null
//e和p.next都是持有對null的引用,即使p.next後來賦予了值
// 只是改變了p.next指向的引用,和e沒有關係
// 把新節點放到鏈表的尾部
p.next = newNode(hash, key, value, null);
//新增時,鏈表的長度大於等於8時,鏈表轉紅黑樹
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 在遍歷過程中, 一直往後移動
p = e;
}
}
//說明新增的元素table中原來就有
//說明新節點的新增位置已經找到了
if (e != null) {
V oldValue = e.value;
// 當 onlyIfAbsent 爲 false 或者原始值爲 null 時,纔會覆蓋值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 當前節點移動到隊尾
afterNodeAccess(e);
// 返回老值
return oldValue;
}
}
// 記錄 HashMap 的數據結構發生了變化
++modCount;
//如果kv的實際大小大於擴容的門檻,開始擴容
//如果 HashMap 的實際大小大於擴容的門檻, 開始擴容
if (++size > threshold)
resize();
// 刪除不經常使用的元素
afterNodeInsertion(evict);
return null;
}
【2】HashMap 查找數據流程
1. 根據hashcode,算出數組的索引,找到槽點
2. 槽點的key和查詢的key相等,直接返回
3. 槽點沒有next,返回null
4. 槽點有next,判斷是紅黑樹還是鏈表
5. 紅黑樹調用find,鏈表不斷循環
HashMap 代碼
public V get(Object key) {
Node<K,V> e;
//調用getNode方法來完成的
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//1:根據hashcode,算出數組的索引,找到槽點
//2:槽點的key和查詢的key相等,直接返回
//3:槽點沒有next,返回null
//4:槽點有next,判斷是紅黑樹還是鏈表
//5:紅黑樹調用find,鏈表不斷循環
final Node<K,V> getNode(int hash, Object key) {
// first 頭結點,e 臨時變量,n 長度,k key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//數組不爲空 && hash算出來的索引下標有值
//table不爲空 && table長度大於0 && table索引位置(根據hash值計算出)節點不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//hash 和 key 的 hash 相等,直接返回
// first的key等於傳入的key則返回first對象
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//hash不等,看看當前節點的 next 是否有值
//first的key不等於傳入的key則說明是鏈表,向下遍歷
if ((e = first.next) != null) {
// 使用紅黑樹的查找
// 判斷是否爲TreeNode,是則爲紅黑樹
// 如果是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 採用自旋方式從鏈表中查找 key,e 爲鏈表的頭節點
do {
// 如果當前節點 hash == key 的 hash,並且 equals 相等,當前節點就是我們要找的節點
//走下列步驟表示是鏈表,循環至節點的key與傳入的key值相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
// 否則,把當前節點的下一個節點拿出來繼續尋找
} while ((e = e.next) != null);
}
}
//找不到符合的返回空
return null;
}
【3】HashMap 刪除數據流程
HashMap 代碼
public V remove(Object key) {
//臨時變量
Node<K,V> e;
/**
* 調用removeNode(hash(key), key, null, false, true)進行刪除,
* 第三個value爲null,表示,把key的節點直接都刪除了,不需要用到值,
* 如果設爲值,則還需要去進行查找操作
*/
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 第一參數爲哈希值,
* 第二個爲key,
* 第三個value,
* 第四個爲是爲true的話,則表示刪除它key對應的value,不刪除key,
* 第四個如果爲false,則表示刪除後,不移動節點
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab 哈希數組,p 數組下標的節點,n 長度,index 當前數組下標
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 哈希數組不爲null,且長度大於0,然後獲得到要刪除key的節點所在是數組下標位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// nodee 存儲要刪除的節點,e 臨時變量,k 當前節點的key,v 當前節點的value
Node<K,V> node = null, e; K k; V v;
// 如果數組下標的節點正好是要刪除的節點,把值賦給臨時變量node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 也就是要刪除的節點,在鏈表或者紅黑樹上,先判斷是否爲紅黑樹的節點
if (p instanceof TreeNode)
// 遍歷紅黑樹,找到該節點並返回
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { // 表示爲鏈表節點,一樣的遍歷找到該節點
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
/**
* 注意,如果進入了鏈表中的遍歷,那麼此處的p不再是數組下標的節點,
* 而是要刪除結點的上一個結點
*/
p = e;
} while ((e = e.next) != null);
}
}
// 找到要刪除的節點後,判斷!matchValue
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果刪除的節點是紅黑樹結構,則去紅黑樹中刪除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果是鏈表結構,且刪除的節點爲數組下標節點,也就是頭結點,直接讓下一個作爲頭
else if (node == p)
tab[index] = node.next;
else
// 爲鏈表結構,刪除的節點在鏈表中,把要刪除的下一個結點設爲上一個結點的下一個節點
p.next = node.next;
// 修改計數器
++modCount;
// 長度減一
--size;
// 此方法在hashMap中是爲了讓子類去實現,主要是對刪除結點後的鏈表關係進行處理
afterNodeRemoval(node);
// 返回刪除的節點
return node;
}
}
// 返回null則表示沒有該節點,刪除失敗
return null;
}
致謝
本博客爲博主的學習實踐總結,並參考了衆多博主的博文,在此表示感謝,博主若有不足之處,請批評指正。