More
More
文章目录

【java源码一带一路系列】之HashMap.putVal()

回顾上期✈观光线路图:putAll() –> putMapEntries() –> tableSizeFor() –> resize() –> hash() –> putVal()…

本期与您继续一起前进:putVal() –> putTreeVal() –> find() –> balanceInsertion() –> rotateLeft()/rotateRight() –> treeifyBin()…


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 为了找到合适的位置插入新节点,源码中进行了一系列比较。
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;
}
}
}

当前节点hash值(ph)与插入节点hash值(h)比较,
若ph > h(dir=-1),将新节点归为左子树;
若ph < h(dir=1),右子树;
否则即表示hash值相等,然后又对key进行了比较。

“kc = comparableClassFor(k)) == null”表示该类本身不可比(class C don’t implements Comparable);“dir = compareComparables(kc, k, pk)) == 0”表示k与pk对应的Class之间不可比。searched为一次性开关仅在p为root时生效,遍历比较左右子树中是否存在于插入节点相等的。

最后比到tieBreakOrder()中的“System.identityHashCode(a) <= System.identityHashCode(b)”,即对象的内存地址来生成的hashCode相互比较。堪称铁杵磨成针的比较。

这里循环的推进是靠“if ((p = (dir <= 0) ? p.left : p.right) == null)”,之前千辛万苦比较出的dir也在这使用。直到为空的左/右子树节点,插入新值(新值插入的过程参考下图理解)。

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}

有没有发现,如果当你看懂putTreeVal,类比find是不是变得很好理解了呢?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true; // x为红
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// x为根
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// x父节点为黑 || 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);
}
}
}
}
}
}

在插入新值后,可能打破了红黑树原有的“平衡”,balanceInsertion()的作用就是要维持这种“平衡”,保证最佳效率。所谓的红黑树“平衡”即:

①:所有节点非黑即红;

②:根为黑,叶子为null且为黑,红的两子节点为黑;

③:任一节点到叶子节点的黑子节点个数相同;


下面,以“(xp == (xppl = xpp.left))”简(chou)单(lou)的给大家画个图例(其中①②与源码注释相对应)。
image


图②中打钩的都是合格的红黑树其实,图②右边方框内为左旋右旋节点关系变化图解。
image


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 左旋 与 右旋
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; // p为pp左子节点
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;
}

左旋右旋过程包含在上面的图例中了,另附上两张网上看到的动图便于大家理解。

image

image

同时,在线红黑树插入删除动画演示【点我】,还不理解的童鞋可以亲自直观的试试。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
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()的treeifyBin()在链表中数目大于等于“TREEIFY_THRESHOLD - 1”时触发。当数目满足MIN_TREEIFY_CAPACITY时,链表将转为红黑树结构,否则继续扩容。treeify()类似putTreeVal()。

至此,HashMap插入告一段落。时间有限,江湖再见。

打赏
手机扫一扫,支持CHE~