常見面試題:HashMap的底層原理?
常見面試題:HashMap的底層原理?
在 JDK7 和 JDK8 中,HashMap 的底層是有所不同的
- 在 JDK7 中,HashMap 是通過陣列+連結串列實現的
- 在 JDK8 中,HashMap 是通過陣列+連結串列+紅黑樹實現的
我們通過原始碼來探討 JDK8 中 HashMap 的底層,主要是分析它的一些屬性,以及它的構造方法、put 方法和 get 方法
屬性
首先看一下 HashMap 裡都有哪些屬性,得有個大致瞭解:
//HashMap底層的那個陣列 transient Node<K,V>[] table; //似乎和快取有關 transient Set<Map.Entry<K,V>> entrySet; //記錄當前HashMap中有多少個Node transient int size; //記錄當前HashMap被修改了多少次,在出現執行緒不安全的問題時可以通過這個屬性來迅速報錯 transient int modCount; //當size達到這個閾值後將對HashMap進行擴容,*****不過在table正式初始化前,用來記錄table的初始容量***** int threshold; //負載因子,在table正式初始化後,threshold就是通過capacity*loadFactor計算得到的 final float loadFactor; //預設的table初始容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //table的最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //預設的負載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //與連結串列升級為紅黑樹有關的一個閾值 static final int TREEIFY_THRESHOLD = 8; //與紅黑樹退化為連結串列有關的一個閾值 static final int UNTREEIFY_THRESHOLD = 6; //與連結串列升級為紅黑樹有關的一個閾值 static final int MIN_TREEIFY_CAPACITY = 64;
構造方法
然後來看看 HashMap 的構造方法,雖然這裡呼叫的是無參的,但我們直接看最複雜的那個:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
-
這兩個引數,一個是初始的陣列容量(無參時預設為16),另一個和擴容有關,叫做負載因子(無參時預設為0.75)
-
這個建構函式其實也沒做什麼事情,就是對輸入的引數進行檢查,然後對 HashMap 的兩個屬性做初始化
-
可以發現,在構造方法中其實並沒有對
table
進行初始化,此時table
的初始容量舊記錄在threshold
中 -
需要注意的是,這裡
loadFactor
是直接賦值的,但threshold
的賦值是經過處理的,看看tableSizeFor
這個方法:/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
- 直接看就懵逼了,因此我們可以帶入幾個值看看,比如說,如果我傳入的 capacity 是 10 的話,這個方法就會返回 16,如果傳入的 17 的話,這個方法就會返回 32,這也符合官方對這個方法的描述
- 由此可見,HashMap 的陣列容量並不是任意的,一定得是 2 的冪,就算我們強行指定陣列容量為 10,也會經過這個方法的處理而變成 16
- 此外,構造方法並沒有限制
initialCapacity
不能為 0,如果是 0 的話經過這個方法返回的還是 0,這其實算是個特殊情況吧,如果initialCapacity
為 0,那麼在後續對 table 初始化時就會使用DEFAULT_INITIAL_CAPACITY
作為其初始容量
put
接下來看一下 put
方法,是最重要的一個方法
首先需要明確,雖然我們是 put 了一個鍵值對,但其實最後存到 HashMap 裡的是一個 Node,這個 Node 有以下屬性:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
看看 put 的原始碼:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
-
可以發現 put 只是個馬甲,實際上是呼叫了 putVal,涉及五個引數,我們關注前三個,除了 key 和 value,還有根據 key 算出來的一個雜湊值,看看到底是怎麼算的:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
可以發現,這個雜湊值並不直接是物件 markword 裡的那個雜湊值,而是經過處理的
-
如果 key 為 null,那麼雜湊值就是 0(說明 HashMap 是允許 null 作為 key 的)
-
否則,先得到 markword 裡的雜湊值,記為 h,然後進行
h ^ (h >>> 16)
這個運算,運算結果才是 HashMap 真正使用的雜湊值
再來看看 putVal 的原始碼,可以發現比較複雜,請看註釋:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//定義一些變數,後面會使用到
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//先對變數進行賦值,tab就是table的另一個引用,n就是table的容量
//如果table還未初始化(或者容量為零),那麼進行初始化(或者擴容),並將新的容量賦值給n
if ((tab = table) == null || (n = tab.length) == 0)
//注意這裡的resize方法,初始化、擴容都是使用的這個方法
n = (tab = resize()).length;
//注意這裡的i = (n - 1) & hash,這裡其實是在計算這個node該放在table的哪個位置中,i就是最後算出的位置
//這裡先將tab[i]處的內容交給了p
//如果發現p為null,說明這個位置還什麼都沒有,此時直接新建一個node放進去就好
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果p不為空,說明這個地方已經有東西了,這裡可能是個連結串列,也可能是個紅黑樹
else {
//定義一些變數
Node<K,V> e;
K k;
//我們有時可能會put幾個相同key的node到HashMap中,此時新的value會覆蓋舊的value
//這裡就是在判斷p的key和當前put的node的key是否相同
//如果是,將p交給e,此時也不用管是連結串列還是紅黑樹,直接覆蓋就完事了
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
//覆蓋操作會在後面統一處理
e = p;
//如果key不相同,那麼需要根據p是連結串列結點還是紅黑樹結點來分別處理
//如果是紅黑樹結點,那麼呼叫putTreeVal方法將新的node插入到紅黑樹中
else if (p instanceof TreeNode)
//如果發現紅黑樹中存在相同key的node,會把這個node交給e,此時其實沒有插入,只需要後面進行覆蓋
//否則返回null,表示成功插入了新的node
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果是連結串列結點
else {
//這裡是在遍歷連結串列,binCount記錄連結串列中有多少個結點
for (int binCount = 0; ; ++binCount) {
//如果發現遍歷到底了,說明連結串列中沒有相同key的node,那麼直接新建一個node插在最後
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果插入之後發現連結串列中結點個數超過了閾值(預設為大於8),將連結串列升級為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1)
//注意,其實升級還需要另一個條件,隱藏在treeifyBin這個方法中,同時滿足才會升級
treeifyBin(tab, hash);
//都插入好了肯定可以break了
break;
}
//如果遍歷途中發現有相同key的node,那麼也可以break了,後面進行覆蓋即可
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//在上面的三種情況中,一旦發現有相同key的node,都會把這個node存放在e中,在這裡進行覆蓋
if (e != null) {
//得到舊的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//用新的value覆蓋舊的
e.value = value;
afterNodeAccess(e);
}
}
//如果確實是插入了node,那麼modCount加一
++modCount;
//size肯定也加一,如果加一後發現超過了閾值,那麼需要擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//如果確實是插入了node還不是覆蓋,put方法返回null
return null;
}
可能會有些迷糊,這裡再捋一下:
-
我們是在已經有了 hash key value 三樣東西之後進入 putVal 方法的
-
首先,檢查 table 是否初始化了,如果初始化了,再檢查其容量是否為零,無論其中哪一種情況發生,顯然這個 HashMap 都是沒法使用的,因此需要對 table 進行初始化或者擴容,這兩個操作統一通過
resize
方法來完成,由於這個方法也比較複雜,因此放到最後再具體介紹 -
現在,一個可用的 table 肯定已經有了,接下來就是根據 hash 來計算該把新的 node 放在 table 的哪個位置,把目標索引記為 i,那麼 i 的計算公式為
i = (n - 1) & hash
,新的 node 就是放在table[i]
這個地方 -
但
table[i]
這個地方可能是原本就有 node 的,不能直接就進行table[i]=newNode
這種莽夫操作,因此,先把table[i]
處的node 賦值給一個變數 p,接下來分四種情況進行討論:-
如果 p 為 null,說明這個地方沒有 node,那是最爽的,直接
table[i]=newNode
就完事了,新的 node 就插入成功了 -
如果 p 不為 null,但 p 的 key 和當前要插入的 node 的 key 相同,說明此時要完成的不是插入,而是覆蓋,於是用新的 value 覆蓋舊的 value 就完事了
-
如果 p 不為 null,p 的 key 和當前要插入的 node 的 key 也不同,此時就沒那麼方便了,因為我們不得不深入到連結串列或者紅黑樹中來進行插入的工作了
-
如果發現 p 是一個紅黑樹結點,那麼就得在這個紅黑樹中進行插入操作,有關操作都被封裝在了
putTreeVal
這個方法中,我們也不必對這個方法過於糾結,因為比較複雜,而且和 HashMap 的關係並不大,我們只需要知道,如果發現樹中存在相同 key 的 node,那麼實際進行的也是覆蓋操作,而不是插入 -
如果發現 p 是一個連結串列結點,那麼就得在這個連結串列中進行插入操作,這相比紅黑樹是比較簡單的,就是遍歷這個連結串列,如果途中發現某個相同 key 的 node,那麼進行覆蓋操作,但如果遍歷到底了也沒有發現相同 key 的 node,那就說明沒有,於是把新的 node 插入到連結串列的尾部即可,在插入完畢後,如果發現這個連結串列中結點的個數超過了閾值(預設為 8),就會呼叫
treeifyBin
這個方法,我們稍微看一下這個方法,可以發現,雖然方法的名字是樹化,但在方法中還會對 table 的容量做判斷,如果 table 的容量小於MIN_TREEIFY_CAPACITY
,那麼進行的其實是陣列擴容,而不是連結串列樹化:final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //注意這裡,如果table的容量小於MIN_TREEIFY_CAPACITY這個閾值(預設為64),其實也不會把連結串列升級為紅黑樹 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
-
-
-
至此,如果上面完成的是覆蓋操作,那麼其實
putVal
方法已經完事了,但如果完成的是插入操作,最後還需要判斷 HashMap 中 node 的個數是否超過了閾值,如果超過了,需要進行一次擴容
現在,我們就剩下 resize
這個方法沒有看了,在看它之前,先梳理一下哪些情況下會呼叫它:
- case1:嘗試插入 node 時發現 table 未初始化,或者 table 的容量為 0(我不是很清楚怎麼會有容量為 0 的情況發生,因此後面分析時為了簡潔起見就不管這種神奇的情況了)
- case2:成功插入新 node 後,發現連結串列中結點個數大於 8,試圖進行連結串列樹化,但發現 table 的容量小於 64
- case3:成功插入新 node 後,發現雜湊表中 node 的個數超過了
threshold
接下來就正式看一下 resize
這個方法:
final Node<K,V>[] resize() {
//記錄舊的table(可能是未初始化的,因為初始化和擴容都是使用的這個方法)
Node<K,V>[] oldTab = table;
//記錄舊table的容量(如果未初始化就是0)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//記錄舊table的閾值(如果未初始化,記錄的是要把table初始化為多大的容量)
int oldThr = threshold;
//新table的容量和閾值,後面一通操作就是給這兩個變數確定值
int newCap, newThr = 0;
//如果舊table是已經初始化的
if (oldCap > 0) {
//如果舊table的容量已經超過最大限制了,那麼已經不能再擴容了,只能放寬擴容的閾值,此時直接返回舊的table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果將舊容量翻倍後依舊沒有超過限制,並且舊容量是大於等於16的,將翻倍後的舊容量和舊閾值交給新容量和新閾值
//****在我們的日常使用中絕大多數情況都是進的這裡,也就是說大多數情況下擴容都是把容量和閾值擴大為原來的兩倍****
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
//以下兩種都是table未初始化的情況,注意這裡的話oldThr的含義就不是舊閾值了,而是要把table初始化為多大的容量
//當我們使用無參的建構函式,或者使用有參並且傳入了正常的initialCapacity,那麼oldThr肯定大於0並且一定是2的冪
else if (oldThr > 0)
//把oldThr交給newCap就完事了
newCap = oldThr;
//這就是前面提到過的特殊情況,當我們使用有參的建構函式但傳入了0作為initialCapacity,表明使用預設值
else {
//使用預設的初始容量
newCap = DEFAULT_INITIAL_CAPACITY;
//使用預設的初始容量和預設的負載因子算出閾值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新容量前面已經全部算好了(如果沒反應過來可以再體會一下),但如果新閾值還沒算出來,統一在這裡計算
if (newThr == 0) {
//先根據新容量和負載因子算出一個閾值ft
float ft = (float)newCap * loadFactor;
//如果新容量和ft都沒有超出限制,就使用ft作為新閾值,否則,使用INT_MAX作為新閾值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//將求出的新閾值交給threshold,此時閾值就真的更新好了,再下面就是根據新容量進行真正的擴容了
threshold = newThr;
//根據新容量開闢新的table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果舊table是未初始化的,裡面肯定沒東西,那麼resize就結束了,否則需要把舊table裡的東西挪到新table裡
if (oldTab != null) {
//遍歷舊table
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果oldTab[j]處為空,那麼這個位置就不用管了,但如果oldTab[j]處不為空,先把這裡的node交給e
if ((e = oldTab[j]) != null) {
//oldTab[j]處肯定直接置空,不需要了
oldTab[j] = null;
//如果只有一個node,那方便,直接把這唯一的一個node放到newTab裡
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//但如果有多個node,就麻煩了
//如果這是棵紅黑樹,那麼所有的轉移操作都封裝在split裡了,這裡就不深入了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果這是個連結串列
else {
//構建兩個新連結串列lo和hi,分別有兩個變數指向首尾
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//遍歷連結串列中的每個node
do {
next = e.next;
//如果雜湊值與舊容量相與結果為0,把node放到lo裡
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//否則,把node放到hi裡
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//收尾工作,把lo和hi放到newTab裡
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
雖然程式碼看起來有點複雜,但其實思路是比較直觀的,無論是為了初始化還是擴容,第一步都是算出接下來新 table 的容量和閾值,如果是為了初始化,那麼開闢好新的 table 就完事了,但如果是為了擴容,還得把舊 table 裡的內容轉移到新 table 裡
現在,我們對整個 put 的流程已經有所瞭解了,但裡面有一些地方其實是比較讓人疑惑的:
-
在計算雜湊值時,為何不直接使用 markword 裡的雜湊值,而是要經過一些額外的運算?
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 首先我們得知道,雜湊值很重要,涉及這個 node 最後會存放在 table 的哪個位置中
- 如果直接使用 markword 裡的雜湊值,要知道我們是可以覆寫一個物件的
hashCode
方法的,如果我們寫的很爛,然後還在這裡直接用,可能會導致最後很多的 node 都放在 table 的同一個位置中,這對 HashMap 的效能影響是很大的 - 因此,在這個方法裡會對 markword 裡的雜湊值做一些額外的運算(移位和異或),目的就是為了讓雜湊值變得更隨機,使得最後各個 node 可以儘量均勻的分佈在 table 中,提高 HashMap 的效能
-
在計算要把 node 放在 table 的哪個位置中時,
i = (n - 1) & hash
是什麼原理,為什麼不直接對 n 取餘?-
首先我們得知道,table 的容量是固定的,比如 16,那麼不管我們的雜湊值有多複雜,經過計算都得落在 0~15 這個範圍中
-
直接用雜湊值對 16 取餘肯定是可行的,但取餘看著簡單,其實是比較耗時的,相比之下位運算的速度快很多
-
那麼,
i = (n - 1) & hash
是怎麼保證計算結果落在 0~15 這個範圍中的呢?-
首先,16 是 table 目前的容量,其二進位制為 10000,減一後為 15,二進位制為 1111
-
我們的雜湊值是一個 32 位的整數,不管它有多大,高位有多少個 1,跟 1111 進行與操作後,相當於取了雜湊值的最後四位,因此結果一定是在 0000~1111 之間的,也就是 0~15,隨便看個例子:
雜湊值 1011 1100 0010 0011 1111 1100 1001 1100 15 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 0000 0000 0000 0000 0000 0000 1100
-
-
既然通過位運算也能達到目標,並且速度還比取餘快,那自然就是採用這種方案了
-
-
為什麼 table 的容量一定得是 2 的冪?
-
直接舉個反例吧,如果 table 的容量為 17,那麼雜湊值經過計算一定得落在 0~16 這個範圍中:
雜湊值 1011 1100 0010 0011 1111 1100 1001 1100 16 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 0000 0000 0000 0000 0000 0001 0000
-
可以發現,在這種情況下,雖然運算結果也不會超出 0~16 的範圍,但其實只會出現 10000 和 00000 兩種結果,也就是說,無論我們的雜湊值是多少,最後這個 node 只會被放到 table[0] 或者 table[16] 的位置,這樣一來,剩下的位置統統都浪費了,所有 node 都扎堆在兩個位置裡,會導致 HashMap 的效能受到極大的影響
-
因此,我們要求 table 的容量一定得是 2 的冪,因為 2 的冪減一之後,其二進位制肯定是 1111 這種形式,與雜湊值進行與操作後,首先肯定不會越界,其次保證了 table 中的每個位置都會被用上
-
-
在擴容的轉移過程中,連結串列中的 node 是如何轉移的?lo 和 hi 這兩個連結串列是什麼意思?
-
首先,我們想一下最暴力的轉移方式是什麼?其實很簡單,就是遍歷 oldTab[i] 處的每個 node,然後根據這個 node 的雜湊值和 table 的新容量來計算出這個 node 在 newTab 中的位置,即
hashCode & (newCapacity - 1)
,然後放進去即可 -
但實際上,一個 node 在 newTab 中的位置只有兩種可能,假如原始容量為 16,新容量為 32,那麼 oldTab[i] 處的 node 只可能被轉移到 newTab[i] 和 newTab[i+16] 兩個位置
-
我們舉個例子,假如原始容量為 16,新容量為 32,有 node1 和 node2 兩個 node:
node1 1011 1100 0010 0011 1111 1100 1001 1100 oldCap-1 0000 0000 0000 0000 0000 0000 0000 1111 oldPos 0000 0000 0000 0000 0000 0000 0000 1100 -> 12 node1 1011 1100 0010 0011 1111 1100 1001 1100 newCap-1 0000 0000 0000 0000 0000 0000 0001 1111 newPos 0000 0000 0000 0000 0000 0000 0001 1100 -> 12+16=28 node2 1011 1100 0010 0011 1111 1100 1000 1100 oldCap-1 0000 0000 0000 0000 0000 0000 0000 1111 oldPos 0000 0000 0000 0000 0000 0000 0000 1100 -> 12 node2 1011 1100 0010 0011 1111 1100 1000 1100 newCap-1 0000 0000 0000 0000 0000 0000 0001 1111 newPos 0000 0000 0000 0000 0000 0000 0000 1100 -> 12
-
可以發現,由於容量總是 2 的冪,並且每次擴容都是翻倍,因此
newCap-1
和oldCap-1
其實只有一位 1 的差別(比如這裡只有第五位有區別),而 node 的雜湊值是不會改變的,因此,在和newCap-1
進行與操作後,結果的後四位不可能發生改變,node 的新位置只取決於雜湊值的第五位是 0 還是 1- 如果是 0(比如 node2),那麼結果的第五位肯定還是 0,因此 node2 還是放在 i 的位置
- 但如果是 1(比如 node1),那麼結果的第五位就變成 1 了,因此會放在 i+16 的位置
-
因此,oldTab[i] 處的 node 只可能被轉移到 newTab[i] 和 newTab[i+16] 兩個位置,於是就產生了 lo 和 hi 這兩個連結串列,在轉移一個 node 時,先通過
hashCode & oldCap
來判斷雜湊值的第五位是 0 還是 1,如果是 0,就加入到 lo 中,如果是 1,就加入到 hi 中,所有 node 全部轉移完成後,再統一把 lo 放到 newTab[i] 的位置,把 hi 放到 newTab[i+16] 的位置,此時整個轉移的過程就結束了
-
get
在把 put 方法看明白後,get 就簡單多了:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以發現 get 的主體是在 getNode
這個方法中,如果可以根據給定的 key,在 HashMap 中找到相同 key 的 node,那麼返回這個 node 的 value,否則返回 null
再來看看 getNode
這個方法:
final Node<K,V> getNode(int hash, Object key) {
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) {
//如果這個位置上的node就是目標,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不止一個node,根據是紅黑樹還是連結串列分別討論
if ((e = first.next) != null) {
//如果是紅黑樹
if (first instanceof TreeNode)
//封裝好了,咱就不管了
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是連結串列
do {
//就是遍歷連結串列,逐個檢查是否有相同key的node,有的話就返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))
return e;
} while ((e = e.next) != null);
}
}
//如果沒有找到相同key的node,只能返回null了
return null;
}
這個方法需要兩個引數,一個就是我們輸入的 key,還有一個是根據 key 計算出的雜湊值
首先進行一系列判斷,主要做了三件事情:
- 保證 table 已經初始化了
- 保證 table 的容量不為 0
- 根據
(n - 1) & hash
算出目標 node 會在 table 的哪個位置中(雜湊值的作用),保證這個位置有 node 存在
如果上述三個條件有一個不滿足,那麼這個 HashMap 裡肯定沒有目標 node 了,直接返回 null 就完事了
否則,查詢過程如下:
- 先看 table[i] 處的 node 是否就是目標 node,如果是,那最爽,直接返回這個 node 即可
- 如果不是目標 node,看看這個 node 有沒有 next,也就是這個位置有沒有多個 node,如果這個位置就一個 node,那麼也挺爽,直接返回 null 即可
- 如果這個位置有多個 node,就麻煩了,需要判斷這裡是連結串列還是紅黑樹
- 如果是紅黑樹,具體查詢細節封裝在
getTreeNode
裡了,咱就不管了 - 如果是連結串列,邏輯很簡單,就是遍歷一次連結串列,看有沒有目標 node,有的話就返回,沒有就返回 null
- 如果是紅黑樹,具體查詢細節封裝在