之前写过一篇HashMap笔记,不过是JDK1.7(含)之前HashMap,JDK在8后对HashMap做了很多的优化,在key冲突情况下的HashMap中的元素会形成一条链表,如果冲突key过多,则链表会越长,查询会变成线性时间复杂度O(log(n)),JDK8加入了红黑树来优化这种情况,所以也进行了再次研究下源码。
我们先从put方法入手。
JDK8的HashMap在插入元素时
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;
//1找到key匹配的节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2没有找到key,但槽位上是树节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//3不是树节点,则还是链表结构
else {
for (int binCount = 0; ; ++binCount) { //A处
//4依次遍历链表的节点
if ((e = p.next) == null) {
//6遍历完链表,key不存在,则新建一个节点
p.next = newNode(hash, key, value, null);
//7加上新的节点,如果链表的节点数量大于8,则将链表转换成树结构。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//5在链表上找到key
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在源码的“A处”,新加一个节点,如果加上新的节点,节点的链表长度超过8个(TREEIFY_THRESHOLD值为8),则会尝试将链表转成树结构(使用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) //A1处
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); //B处
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null) //C处
hd.treeify(tab);
}
}
图解:
为什么我在上面说是尝试转成树结构呢?
从treeifyBin方法的“A1”处的源码可以看到,当整个HashMap的Node<K,V>[] table的长度还没达到最小转换树容量(MIN_TREEIFY_CAPACITY = 64)时,只会对整个HashMap进行扩容resize不会将链表转成树;当达到了MIN_TREEIFY_CAPACITY的容量后,才会对table[index = (n - 1) & hash]的链表进行转换树结构的操作,
MIN_TREEIFY_CAPACITY的值,刚好是4 * TREEIFY_THRESHOLD(值是8),刚好是链表转树节点数的4倍,这里设置了4倍,如果设置过小的话,再次resize后可能节点计算得到的槽位冲突,过大的话,可能导致一个槽位的元素过多;
在源码的“B处”生成一个树节点,只是纯粹一个树节点,节点与节点之前形成双向链表,还没有生成树的左子树和右子树。
在“B处”这里的do ... while语句块主要是将链表里的节点转换成单独的树节点,树节点之间先用prev和next和链接起来,树节点结构:
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
TreeNode的结构定义:
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;
}
下面才是将节点组装成一棵树,
在“B处”已经将节点全部换成树节点后,“C处”开始将链表里的单独的树节点连接成一棵树(使用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) { //D1处
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false; //根节点是黑色节点
root = x; //D2,链表的首节点设为树的根节点
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) { //D9
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) //D3
dir = -1;
else if (ph < h) //D4
dir = 1;
else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) //D5
dir = tieBreakOrder(k, pk); //D6
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) { //D7
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x); //D8
break;
}
}
}
}
moveRootToFront(tab, root);
}
在“D1处”,TreeNode<K,V> x = this,是说明从链表的头节点开始来组装树的,“x = next”,则是循环下一个节点。
在“D2处”,链表的头节点作为树的根节点,根节点是黑色的,接着循环下一个节点,并跟根节点做比较。
从D3-D5,则是节点和根节点进行比较,
其中D3,D4是比较节点的hash值,如果hash值无法区分大小,则继续看"D5",
在“D5”,看节点key是实现了Comparable接口,即是comparableClassFor(k)方法获取Comparable Class,
如果节点key实现了Comparable接口,则调用Comparable的compareTo(Object o)方法和根节点key比较,这一过程是在compareComparables(kc, k, pk)方法完成;
如果节点key没实现Comparable接口或者调用了compareTo还是无法区别大小,则看"D6";
在“D6”,tieBreakOrder方法,会先根据节点key和和根节点key的class name来比较大小,也就是String类的compareTo方法(String实现了compareTo方法),如果还不能区分大小,最终会使用System.identityHashCode(Object x)方法(这个方法是根据对象在内存中的地址来计算出一个值)来比较大小。
总结来说,经过D3或D4或D5或D6之后,肯定能分出当前节点和根节点对比大小:
hash值->Comparable接口->key Class name->System.identityHashCode()
通过以上顺序来得到dir值。
所以在上面这么复杂的过程,就是为了比较节点和根节点的大小来决定先后顺序,不能使两者相等,从而组装成树。
在“D7处”,如果节点比根节点小,则往根节点的左子树p=p.left找,否则往根节点的右子树p=p.right找,左(右)节点不为空时则回到循环D9,当前节点继续和左(右)节点对比,直接左(右)节点为空null时,即当前节点作为新的左(右)子树,并且当前节点的父节点指向旧的左(右)节点。
总结来说,当前节点和已经排在红黑树里的节点逐一比对,然后插入到合适的位置。
其中在“D8处”,当一个节点插入红黑树作为新节点时,需要根据红黑树的规则处理整棵树的平衡,因为红黑树的查找时间比一般二叉树更优。下面看看balanceInsertion方法,是插入元素时如何处理红黑树平衡的。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
x.red = true; //E1,新插入红黑树的节点(根节点除外)默认是红色
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) { //E2,找到节点的父节点
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null) //E3,父节点是黑色,否则,父节点的父节点为null,即父节点是根节点
return root;
if (xp == (xppl = xpp.left)) { //E4,父节点是左节点
if ((xppr = xpp.right) != null && xppr.red) { //E5,父节点的兄弟节点(右节点或者说是叔父节点)存在,并且是红色的
xppr.red = false; //叔父节点红色改成黑色
xp.red = false; //父节点点红色改成黑色
xpp.red = true; //父节点的父节点改成红色
x = xpp; //指向父节点的父节点
}
else { //E6,叔父节点为null,或者叔父节点是黑色的
if (x == xp.right) { //E7,当前节点是右节点
root = rotateLeft(root, x = xp); //以父节点为支点,左旋
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //E8,有父节点
xp.red = false; //改颜色,父节点改为黑色
if (xpp != null) { //E9,父节点也有父节点
xpp.red = true; //改颜色,父节点的父节点改为红色
root = rotateRight(root, xpp); //E10,以父节点的父节点为支点,右旋
}
}
}
}
else { //E11,父节点是右节点
if (xppl != null && xppl.red) { //E12,叔父节点存在,并且是红色的
xppl.red = false; //改变颜色,叔节点改为黑色
xp.red = false; //父节点改为黑色
xpp.red = true; //父节点的父节点改为红色
x = xpp; //指向父节点的父节点
}
else { //E13,叔父节点不存在,或者是黑色的
if (x == xp.left) { //E14,当前节点是左节点
root = rotateRight(root, x = xp); //以父节点为支点,右旋
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //E15,有父节点
xp.red = false; //改颜色,父节点改为黑色
if (xpp != null) { //E16,父节点也有父节点
xpp.red = true; //改颜色,父节点的父节点改为红色
root = rotateLeft(root, xpp); //以父节点的父节点为支点,左旋
}
}
}
}
}
}
源码“E1”处,新插入红黑树的节点默认是红色的(新插入树的根节点是黑色除外),
左旋源码
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) { //p节点有右节点r
if ((rl = p.right = r.left) != null) //右节点r的左节点变为p节点的右节点rl
rl.parent = p; //设置rl的父节点为p节点
if ((pp = r.parent = p.parent) == null) //如果p的父节点为null,那么p就是树的根节点
(root = r).red = false; //p的右节点r变为树新的根节点,并且根据红黑树的规则将r节点的颜色改为黑色
else if (pp.left == p) //如果p是左节点,则p的右节点r变为p的父节点的左节点,即r取代p的位置
pp.left = r;
else
pp.right = r; //否则p是右节点,则p的右节点r变为p的父节点的右节点
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) { //p节点有左节点l
if ((lr = p.left = l.right) != null) //左节点l的右节点变为p节点的左节点lr
lr.parent = p; //设置lr的父节点为p节点
if ((pp = l.parent = p.parent) == null) //如果p的父节点为null,那么p就是树的根节点
(root = l).red = false; //p的左节点l变为树新的根节点,并且根据红黑树的规则将l节点的颜色改为黑色
else if (pp.right == p) //如果p是右节点,则p的左节点l变为p的父节点的右节点,即l取代p的位置
pp.right = l;
else
pp.left = l; //否则p是左节点,则p的左节点l变为p的父节点的左节点
l.right = p;
p.parent = l;
}
return root;
}
右旋看下图:
JDK1.8的hash计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
h >>> 16,即是hash值的高16位往右移到最低的16位,最高16变0;
(h = key.hashCode()) ^ (h >>> 16),即是高16位和低16位异或运算;异或运算:相同为0,不同为1.