JDK1.8版本HashMap源码分析之put篇
HashMap的put()方法、putVal()方法
HashMap的treeifyBin方法TreeNode的treeify()方法TreeNode的balanceInsertion()方法TreeNode的旋转方法TreeNode的moveRootToFront()方法TreeNode的putTreeVal()方法
HashMap的put()方法、putVal()方法
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
,
boolean evict
) {
Node
<K,V>[] tab
; Node
<K,V> p
; int n
, i
;
if ((tab
= table
) == null
|| (n
= tab
.length
) == 0)
n
= (tab
= resize()).length
;
if ((p
= tab
[i
= (n
- 1) & hash
]) == null
)
tab
[i
] = newNode(hash
, key
, value
, null
);
else {
Node
<K,V> e
; K k
;
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
) {
if ((e
= p
.next
) == null
) {
p
.next
= newNode(hash
, key
, value
, null
);
if (binCount
>= TREEIFY_THRESHOLD
- 1)
treeifyBin(tab
, hash
);
break;
}
if (e
.hash
== hash
&&
((k
= e
.key
) == key
|| (key
!= null
&& key
.equals(k
))))
break;
p
= e
;
}
}
if (e
!= null
) {
V oldValue
= e
.value
;
if (!onlyIfAbsent
|| oldValue
== null
)
e
.value
= value
;
afterNodeAccess(e
);
return oldValue
;
}
}
++modCount
;
if (++size
> threshold
)
resize();
afterNodeInsertion(evict
);
return null
;
}
在上述方法中,涉及到三种情况:第一种情况,数组索引位置没有键值对,处理方式就是直接把待添加键值对封装成Node添加到索引位置即可;第二种情况,如果数组索引位置有键值对,而且封装的TreeNode节点,处理方式是调用红黑树的插入方法,把待添加键值对添加到红黑树中;第三种情况,同样数组索引位置有键值对,但是封装的是Node节点,处理方式就比较复杂,首先把待添加键值对封装成Node节点添加到链表尾部,然后判断当前链表长度,如果到达阈值(TREEIFY_THRESHOLD变量),就判断是扩容还是转换为红黑树(treeifyBin方法)。 我们循序渐进的看,首先,第一种情况不需要再分析,我们先来分析第三种情况,因为第二种情况其实是第二种情况的后续。
HashMap的treeifyBin方法
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
);
}
}
从上述方法中,我们知道当链表长度到达一定阈值(默认为8)时,会调用一个方法,判断是否需要转换红黑树,判断的依据就是构成HashMap的数组长度是否达到阈值(默认为64),如果还没有,那么只进行扩容即可,不会转换。真正的转换红黑树的方法是treeify方法,这个方法是TreeNode的方法,接下来具体分析该方法。
TreeNode的treeify()方法
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
);
}
上述这个方法,比较复杂,首先这个方法是TreeNode的方法,由一个TreeNode节点调用。我们首先要知道一点,那就是转换成红黑树之前的链表,顺序是按照添加的先后顺序链接的。红黑树,它首先是一颗二叉查找树,然后才是红黑树,所以它对节点的大小是有要求的。然后我们分析这段代码,该方法,由链表头部的节点调用,对于它在树中的位置,是不确定的,所以,这里先把它当做根节点,然后,依次遍历链表的每个节点,插入到树的叶子节点上,没插入一个节点,就平衡一下红黑树(调用balanceInsertion方法),保证红黑树特征。 接下来我们看一下,如何平衡红黑树,同样,该方法也是TreeNode的方法
TreeNode的balanceInsertion()方法
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
);
}
}
}
}
}
}
看上述代码,首先,把添加的节点定义成红色,因为,这样对当前平衡的红黑树侵入最小的,更大限度的满足红黑树特征,我们回顾一下红黑树的特征:第一,满足每个节点要么是黑色要么是红色;第二,根节点是黑色;第三,每个叶子节点是黑色,这里的叶子节点和标准二叉查找树的叶子节点不同,这里指的是为null的叶子节点;第四,红色节点的子节点必须是黑色的;第五,从任何一个节点开始,到它任何一个子孙节点的路径上的黑色节点数量必须一样。通过这些特征,我们发现,把添加的节点定为红色,只可能违反第四条特征,也就是说红色节点的子节点必须是黑色,说白了,我们只需要考虑插入节点的父节点是不是红色。从上边的代码我们也看出来了,所有的操作(变色、旋转)其实都是针对这种情况做的调整。接下来我们简单了解一下旋转的代码
TreeNode的旋转方法
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
;
}
关于左右旋转,不好理解的话,可以自己边画图边理解,还是比较简单的,这里不做阐述了。
到这里,关于链表转换为红黑树的逻辑我们已经分析的差不多了,还差最后一个方法,moveRootToFront(),该方法的作用是
TreeNode的moveRootToFront()方法
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
);
}
}
好了,以上就是链表变成树的最后一步。 至此,链表如何变成树,我们全都剖析完毕。我们来整理一下整体的流程,首先,新的键值对会添加到链表的尾部,达到阈值,触发变成树的检查,当不满足条件的时候,也就是默认数组长度不够64时,只是做了扩容操作,如果满足条件,就会触发变成树的操作,首先把原来无序链表的节点重新封装,然后从头遍历,取出第一个作为根节点,然后做添加操作,每添加一个键值对,都可能导致树结构的错乱,需要平衡这棵树,直到所有节点都添加到树中,最后确保索引位置的节点是这棵树的根节点,整个流程就完了。 最后我们分析一下添加时的另外一个流程,如果这个索引下已经是一棵红黑树了,如何处理呢?其实了解过之前的逻辑,这块就很简单了,直接封装键值对为TreeNode,然后调用树添加方法即可。我们来看看是不是这么做的。
TreeNode的putTreeVal()方法
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
;
}
}
}
看到上边这段代码,是不是觉得简单了很多,很多代码都是和上边相同的。所以不做过多解释了。 至此,整个添加逻辑就分析完了~