JavaSE_HashMap 原始碼詳解 put, get 方法
參考文章:
java 8 Hashmap深入解析 —— put get 方法原始碼
一直以來我是對讀原始碼很不理解的,直到昨天去解讀了 部分的 HashMap 原始碼,發現原始碼中有很多精巧的設計。體現了很多語言層次深入的東西。
對於普通的程式設計師,可能僅僅能說出HashMap執行緒不安全,允許key、value為null,以及不要求執行緒安全時,效率上比HashTable要快一些。
稍微好一些的,會對具體實現有過大概瞭解,能說出HashMap由陣列+連結串列+RBT實現,並瞭解HashMap的擴容機制。
但如果你真的有一個刨根問題的熱情,那麼你肯定會想知道具體是如何一步步實現的。HashMap的原始碼一共2000多行,很難在這裡每一句都說明,但這篇文章會讓你透徹的理解到我們平時常用的幾個操作下,HashMap是如何工作的。
要先提一下的是,我看過很多講解HashMap原理的文章,有一些講的非常好,但這些文章習慣於把原始碼和邏輯分析分開,導致出現了大段的文字講解程式碼,閱讀起來有些吃力和枯燥。所以我想嘗試另一種風格,將更多的內容寫進註釋裡,可能看起來有些囉嗦,但對於一些新手的理解,應該會有好的效果。
好了,開始進入原始碼解讀。
HashMap結構
首先是瞭解HashMap的幾個核心成員變數(以下均為jdk原始碼):
transient Node<K,V>[] table; //HashMap的雜湊桶陣列,非常重要的儲存結構,用於存放表示鍵值對資料的Node元素。 transient Set<Map.Entry<K,V>> entrySet; //HashMap將資料轉換成set的另一種儲存形式,這個變數主要用於迭代功能。 transient int size; //HashMap中實際存在的Node數量,注意這個數量不等於table的長度,甚至可能大於它,因為在table的每個節點上是一個連結串列(或RBT)結構,可能不止有一個Node元素存在。 transient int modCount; //HashMap的資料被修改的次數,這個變數用於迭代過程中的Fail-Fast機制,其存在的意義在於保證發生了執行緒安全問題時,能及時的發現(操作前備份的count和當前modCount不相等)並丟擲異常終止操作。 int threshold; //HashMap的擴容閾值,在HashMap中儲存的Node鍵值對超過這個數量時,自動擴容容量為原來的二倍。 final float loadFactor; //HashMap的負載因子,可計算出當前table長度下的擴容閾值:threshold = loadFactor * table.length。
那麼這些變數的預設值都是多少呢?我們再看一下HashMap定義的一些常量:
//預設的初始容量為16,必須是2的冪次 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量即2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //預設載入因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //當put一個元素時,其連結串列長度達到8時將連結串列轉換為紅黑樹 static final int TREEIFY_THRESHOLD = 8; //連結串列長度小於6時,解散紅黑樹 static final int UNTREEIFY_THRESHOLD = 6; //預設的最小的擴容量64,為避免重新擴容衝突,至少為4 * TREEIFY_THRESHOLD=32,即預設初始容量的2倍 static final int MIN_TREEIFY_CAPACITY = 64;
其次 HashMap的底層實現是基於一個Node的陣列,那麼Node是什麼呢?在HashMap的內部可以看見定義了這樣一個內部類:
Node類 :
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
除此之外,在put 的過程中還用到了 TreeNode 類,我們看下 treenode 的原始碼:
/* ------------------------------------------------------------ */
// Tree bins
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
/**
* Calls find for root node.
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Tie-breaking utility for ordering insertions when equal
* hashCodes and non-comparable. We don't require a total
* order, just a consistent insertion rule to maintain
* equivalence across rebalancings. Tie-breaking further than
* necessary simplifies testing a bit.
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* Forms tree of the nodes linked from this node.
* @return root of tree
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* @param map the map
* @param tab the table for recording bin heads
* @param index the index of the table being split
* @param bit the bit of hash to split on
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (x.red) {
x.red = false;
return root;
}
else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
if (xpr == null)
x = xp;
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
/**
* Recursive invariant check
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
}
可以看到TreeNode 其實就是紅黑樹的一個實現:
需要注意的點:
在HashMap內部定義的幾個變數,包括桶陣列本身都是transient修飾的,這代表了他們無法被序列化,而HashMap本身是實現了Serializable介面的。這很容易產生疑惑:
HashMap是如何序列化的呢?
查了一下原始碼發現,HashMap內有兩個用於序列化的函式 readObject(ObjectInputStream s) 和 writeObject(ObjectOutputStreams),通過這個函式將table序列化。
HashMap 的 put 方法解析
以上就是我們對HashMap的初步認識,下面進入正題,看看HashMap是如何新增、查詢與刪除資料的。
首先來看put方法,我儘量在每行都加註釋闡明這一行的含義,讓閱讀起來更容易理解。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, //這裡onlyIfAbsent表示只有在該key對應原來的value為null的時候才插入,也就是說如果value之前存在了,就不會被新put的元素覆蓋。
boolean evict) { //evict引數用於LinkedHashMap中的尾部操作,這裡沒有實際意義。
Node<K,V>[] tab; Node<K,V> p; int n, i; //定義變數tab是將要操作的Node陣列引用,p表示tab上的某Node節點,n為tab的長度,i為tab的下標。
if ((tab = table) == null || (n = tab.length) == 0) //判斷當table為null或者tab的長度為0時,即table尚未初始化,此時通過resize()方法得到初始化的table。
n = (tab = resize()).length; //這種情況是可能發生的,HashMap的註釋中提到:The table, initialized on first use, and resized as necessary。
if ((p = tab[i = (n - 1) & hash]) == null) //此處通過(n - 1) & hash 計算出的值作為tab的下標i,並另p表示tab[i],也就是該連結串列第一個節點的位置。並判斷p是否為null。
tab[i] = newNode(hash, key, value, null); //當p為null時,表明tab[i]上沒有任何元素,那麼接下來就new第一個Node節點,呼叫newNode方法返回新節點賦值給tab[i]。
else { //下面進入p不為null的情況,有三種情況:p為連結串列節點;p為紅黑樹節點;p是連結串列節點但長度為臨界長度TREEIFY_THRESHOLD,再插入任何元素就要變成紅黑樹了。
Node<K,V> e; K k; //定義e引用即將插入的Node節點,並且下文可以看出 k = p.key。
if (p.hash == hash && //HashMap中判斷key相同的條件是key的hash相同,並且符合equals方法。這裡判斷了p.key是否和插入的key相等,如果相等,則將p的引用賦給e。
((k = p.key) == key || (key != null && key.equals(k)))) //這一步的判斷其實是屬於一種特殊情況,即HashMap中已經存在了key,於是插入操作就不需要了,只要把原來的value覆蓋就可以了。
e = p; //這裡為什麼要把p賦值給e,而不是直接覆蓋原值呢?答案很簡單,現在我們只判斷了第一個節點,後面還可能出現key相同,所以需要在最後一併處理。
else if (p instanceof TreeNode) //現在開始了第一種情況,p是紅黑樹節點,那麼肯定插入後仍然是紅黑樹節點,所以我們直接強制轉型p後呼叫TreeNode.putTreeVal方法,返回的引用賦給e。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //你可能好奇,這裡怎麼不遍歷tree看看有沒有key相同的節點呢?其實,putTreeVal內部進行了遍歷,存在相同hash時返回被覆蓋的TreeNode,否則返回null。
else { //接下里就是p為連結串列節點的情形,也就是上述說的另外兩類情況:插入後還是連結串列/插入後轉紅黑樹。另外,上行轉型程式碼也說明了TreeNode是Node的一個子類。
for (int binCount = 0; ; ++binCount) { //我們需要一個計數器來計算當前連結串列的元素個數,並遍歷連結串列,binCount就是這個計數器。
if ((e = p.next) == null) { //遍歷過程中當發現p.next為null時,說明連結串列到頭了,直接在p的後面插入新的連結串列節點,即把新節點的引用賦給p.next,插入操作就完成了。注意此時e賦給p。
p.next = newNode(hash, key, value, null); //最後一個引數為新節點的next,這裡傳入null,保證了新節點繼續為該連結串列的末端。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //插入成功後,要判斷是否需要轉換為紅黑樹,因為插入後連結串列長度加1,而binCount並不包含新節點,所以判斷時要將臨界閾值減1。
treeifyBin(tab, hash); //當新長度滿足轉換條件時,呼叫treeifyBin方法,將該連結串列轉換為紅黑樹。
break; //當然如果不滿足轉換條件,那麼插入資料後結構也無需變動,所有插入操作也到此結束了,break退出即可。
}
if (e.hash == hash && //在遍歷連結串列的過程中,我之前提到了,有可能遍歷到與插入的key相同的節點,此時只要將這個節點引用賦值給e,最後通過e去把新的value覆蓋掉就可以了。
((k = e.key) == key || (key != null && key.equals(k)))) //老樣子判斷當前遍歷的節點的key是否相同。
break; //找到了相同key的節點,那麼插入操作也不需要了,直接break退出迴圈進行最後的value覆蓋操作。
p = e; //在第21行我提到過,e是當前遍歷的節點p的下一個節點,p = e 就是依次遍歷連結串列的核心語句。每次迴圈時p都是下一個node節點。
}
}
if (e != null) { // existing mapping for key //左邊註釋為jdk自帶註釋,說的很明白了,針對已經存在key的情況做處理。
V oldValue = e.value; //定義oldValue,即原存在的節點e的value值。
if (!onlyIfAbsent || oldValue == null) //前面提到,onlyIfAbsent表示存在key相同時不做覆蓋處理,這裡作為判斷條件,可以看出當onlyIfAbsent為false或者oldValue為null時,進行覆蓋操作。
e.value = value; //覆蓋操作,將原節點e上的value設定為插入的新value。
afterNodeAccess(e); //這個函式在hashmap中沒有任何操作,是個空函式,他存在主要是為了linkedHashMap的一些後續處理工作。
return oldValue; //這裡很有意思,他返回的是被覆蓋的oldValue。我們在使用put方法時很少用他的返回值,甚至忘了它的存在,這裡我們知道,他返回的是被覆蓋的oldValue。
}
}
++modCount; //收尾工作,值得一提的是,對key相同而覆蓋oldValue的情況,在前面已經return,不會執行這裡,所以那一類情況不算資料結構變化,並不改變modCount值。
if (++size > threshold) //同理,覆蓋oldValue時顯然沒有新元素新增,除此之外都新增了一個元素,這裡++size並與threshold判斷是否達到了擴容標準。
resize(); //當HashMap中存在的node節點大於threshold時,hashmap進行擴容。
afterNodeInsertion(evict); //這裡與前面的afterNodeAccess同理,是用於linkedHashMap的尾部操作,HashMap中並無實際意義。1
return null; //最終,對於真正進行插入元素的情況,put函式一律返回null。
}
在上述程式碼中的第十行,HashMap根據 (n - 1) & hash 求出了元素在node陣列的下標。這個操作非常精妙,下面我們仔細分析一下計算下標的過程,主要分三個階段:計算hashcode、高位運算和取模運算。
首先,傳進來的hash值是由put方法中的hash(key)產生的(上述第2行),我們來看一下hash()方法的原始碼:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這裡通過key.hashCode()計算出key的雜湊值,然後將雜湊值h右移16位,再與原來的h做異或^運算——這一步是高位運算。設想一下,如果沒有高位運算,那麼hash值將是一個int型的32位數。而從2的-31次冪到2的31次冪之間,有將近幾十億的空間,如果我們的HashMap的table有這麼長,記憶體早就爆了。所以這個雜湊值不能直接用來最終的取模運算,而需要先加入高位運算,將高16位和低16位的資訊"融合"到一起,也稱為"擾動函式"。這樣才能保證hash值所有位的數值特徵都儲存下來而沒有遺漏,從而使對映結果儘可能的鬆散。最後,根據 n-1 做與操作的取模運算。這裡也能看出為什麼HashMap要限制table的長度為2的n次冪,因為這樣,n-1可以保證二進位制展示形式是(以16為例)0000 0000 0000 0000 0000 0000 0000 1111。在做"與"操作時,就等同於擷取hash二進位制值得後四位資料作為下標。這裡也可以看出"擾動函式"的重要性了,如果高位不參與運算,那麼高16位的hash特徵幾乎永遠得不到展現,發生hash碰撞的機率就會增大,從而影響效能。
HashMap的put方法的原始碼實現就是這樣了,整理思路非常連貫。這裡面有幾個函式的原始碼(比如resize、putTreeValue、newNode、treeifyBin)限於篇幅原因,就不貼了,後面應該還會更新在其他部落格裡,有興趣的同學也可以自己挖掘一下。
HashMap 的 get 方法解析
讀完了put的原始碼,其實已經可以很清晰的理清HashMap的工作原理了。接下來再看get方法的原始碼,就非常的簡單:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value; //根據key及其hash值查詢node節點,如果存在,則返回該節點的value值。
}
final Node<K,V> getNode(int hash, Object key) { //根據key搜尋節點的方法。記住判斷key相等的條件:hash值相同 並且 符合equals方法。
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && //根據輸入的hash值,可以直接計算出對應的下標(n - 1)& hash,縮小查詢範圍,如果存在結果,則必定在table的這個位置上。
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k)))) //判斷第一個存在的節點的key是否和查詢的key相等。如果相等,直接返回該節點。
return first;
if ((e = first.next) != null) { //遍歷該連結串列/紅黑樹直到next為null。
if (first instanceof TreeNode) //當這個table節點上儲存的是紅黑樹結構時,在根節點first上呼叫getTreeNode方法,在內部遍歷紅黑樹節點,檢視是否有匹配的TreeNode。
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash && //當這個table節點上儲存的是連結串列結構時,用跟第11行同樣的方式去判斷key是否相同。
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null); //如果key不同,一直遍歷下去直到連結串列盡頭,e.next == null。
}
}
return null;
}
因為查詢過程不涉及到HashMap的結構變動,所以get方法的原始碼顯得很簡潔。核心邏輯就是遍歷table某特定位置上的所有節點,分別與key進行比較看是否相等。
----------------------------------------------------------------------------------
以上便是HashMap最常用API的原始碼分析,除此之外,HashMap還有一些知識需要重點學習:擴容機制、併發安全問題、內部紅黑樹的實現。這些內容我也會在之後陸續發文分析,希望可以幫讀者徹底理解HashMap的原理。