承接上一篇《JDK1.8源码解析-HashMap I》,本篇主要介绍关于Java8针对HashMap在数据结构上的优化,涉及如何将链表优化成红黑树以及对红黑树的操作。
在上一篇中我们基于put方法分析了HashMap的底层实现,并且知道当hash产生碰撞,HashMap会以链表存放这些keyHash相同的键值对,并且当链表长度大于等于TREEIFY_THRESHOLD
时,会将该链表转换成红黑树;另一方面,put元素时,当哈希桶中本身已经是红黑树(通过首元素instanceof TreeNode
判断),调用TreeNode的putTreeVal
方法插入红黑树。
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
// 如果table的长度小于最小的转换容量阈值,则通过哈希桶的扩容来达解决hash碰撞的问题
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// e是桶中的首元素
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 准备树中的临时变量, 头hd, 尾tl
TreeNode hd = null, tl = null;
// 当
do {
// 将当前链表元素Node转为TreeNode
TreeNode p = replacementTreeNode(e, null);
// 第一次循环,将hd指向链表头元素的地址
if (tl == null)
hd = p;
// 后续循环中,将当前TreeNode的prev指向前一循环的TreeNode
// 前一循环的TreeNode的next指向当前TreeNode
else {
p.prev = tl;
tl.next = p;
}
// tl指向当前TreeNode
tl = p;
} while ((e = e.next) != null);
// 重新规整以tab[index]为头的红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin
首先确定是否要通过红黑树转换方法解决hash碰撞,在桶容量小于MIN_TREEIFY_CAPACITY
的情况下,通过resize()
扩容解决碰撞,因为绝大多数hash碰撞主要由于hash&len-1
相同导致(取模),而非真正的keyHash相同导致,
而当cap很小时,这种情况将更加普遍,因此通过扩容,可以将部分原来在同一链表中的元素扩散到其他桶位置(上一篇resize分析中已经说过,对于这种情况,部分键值对将分布到hash&len-1 + oldCap
的位置)。接下来,如果确实需要转换红黑树,则先预处理链表中的元素,将他们都把类型转换为TreeNode并把他们链接起来,再通过treeify
方法将他们树化,形成符合红黑树特性的一棵平衡树。下面,我们再看看treeify
怎么实现红黑树建立算法。
final void treeify(Node[] tab) {
TreeNode root = null;
// 开始遍历从当前树节点开始直至叶节点
for (TreeNode x = this, next; x != null; x = next) {
// next指向当前节点的next
next = (TreeNode)x.next;
// 初始化left & right
x.left = x.right = null;
// 第一次循环, 将root指向当前节点x
// 根节点的父节点为null
// 根节点为黑色节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class> kc = null;
// 从根节点开始查找需要插入当前x的地方
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
// 父节点的hash大于本节点hash,本节点为left
if ((ph = p.hash) > h)
dir = -1;
// 父节点的hash小于本节点hash,本节点为righ
else if (ph < h)
dir = 1;
// 父节点hash等于本节点hash,包含两种情况
else if ((kc == null &&
// k非Comparable<>类型
(kc = comparableClassFor(k)) == null) ||
// 或者k是Comparable,并且根据compare方法比较返回0
(dir = compareComparables(kc, k, pk)) == 0)
// 处理相等父hash=节点hash,处理规则只要做到保持一致即可,即当出现相同情况始终返回一致性的结果
dir = tieBreakOrder(k, pk);
// 缓存p!=null赋给xp
TreeNode xp = p;
// 根据dir决定p指向left或者right
// 如果p!=null,继续循环查找
// 如果p==null,则p即为需要插入x的地方
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 当前x的父指向xp
x.parent = xp;
// dir<=0 x为left
if (dir <= 0)
xp.left = x;
else
// dir>0,x为right
xp.right = x;
// 插入后对可能破坏红黑树特性的情况进行重新平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
// 经过balance(左旋or右旋),哈希桶中的头元素可能不是root
// 因此需要将root移动到头部,该方法仅仅改变prev,next指针,对树形结构无影响
moveRootToFront(tab, root);
}
treeify
主要将root开始的链表元素根据红黑树规则进行树化,根据平衡树算法先插入二叉查找树,再根据平衡规则重新平衡该树,平衡后的结果可能由于左旋右旋使得原先的root不再是root,所以最后需要将root移动到哈希桶的头部,保证第一个元素是root,移动root的过程仅仅改变了相关元素的prev和next指针,不涉及left,right,parent,red等TreeNode属性,因此不会改变红黑树结构,因此,之后对以头元素为root的红黑树增删改查仍然保持O(logN)复杂度。这部分的最后,我们看下,平衡算法的实现:
static TreeNode balanceInsertion(TreeNode root,
TreeNode x) {
// 按照算法先将插入的节点变成红色
x.red = true;
// xp父节点
// xpp祖父节点
// xppl,xppr 祖父节点的子节点,叔叔节点
for (TreeNode xp, xpp, xppl, xppr;;) {
// 父节点为null, x为root
// root为黑节点,返回x
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 父节点为黑,祖父节点为null,返回原root
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 父节点是祖父节点的左节点,且为红色
if (xp == (xppl = xpp.left)) {
// 祖父节点的右节点不为null,并且xppr是红节点,即叔叔节点为红色
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);
// 如果当前节点(之前的父节点)的父节点为空,那么祖父节点为null
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);
}
}
}
}
}
}
该部分完全是Java对CLRS中红黑树的算法的实现,同样的实现在java.util.TreeMap也有体现。这部分实现实际上与HashMap关系不那么密切了,因此不作详细分析了,有兴趣的读者可以参考红黑树(一)之 原理和算法详细介绍[1]的解释,也可以对比原著的伪代码进行学习。
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
Class> kc = null;
boolean searched = false;
// 获取当前节点的根节点
TreeNode root = (parent != null) ? root() : this;
// 遍历树查找需要插入元素的节点
for (TreeNode p = root;;) {
int dir, ph; K pk;
// 如果当前节点hash>插入元素hash, 走左树
if ((ph = p.hash) > h)
dir = -1;
// 如果当前节点hash<插入元素hash, 走右树
else if (ph < h)
dir = 1;
// 如果当前节点==带插入元素key(对象相等)或者当前节点Key equals 插入元素key
// 返回当前节点,即返回需要插入元素在树中的位置
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 不符合上面对象相等或者equals返回false,但是keyhash相等
else if ((kc == null &&
// k非Comparable<>类型
(kc = comparableClassFor(k)) == null) ||
// 或者k是Comparable,并且根据compare方法比较返回0
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode q, ch;
searched = true;
// 处理真正的hash碰撞
// 不同的对象由于hash算法可能造成碰撞,这种碰撞与HashMap的hash映射算法无关
// 例如:重写K类的hashCode()方法,使所有的对象都返回1,那么任意K的实例均会造成hash碰撞
// 而这种碰撞是HashMap层面无法避免的,因此下面的分支就可以处理这种极端情况。
// 当然现实场景中,如果hashCode利用MD5,SHA1等已经被证明存在碰撞的哈希算法, 也会进入该分支。
// 分别查找左右树,查找相同的对象,如果找不到,说明确实没有相同对象存在
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;
}
// 通过约定的法则计算左右分支,该逻辑与treeify中相同。
dir = tieBreakOrder(k, pk);
}
TreeNode xp = p;
// 不断寻找树的分支直至叶节点,左节点或者右节点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 缓存当前p的next
Node xpn = xp.next;
// 创建新的TreeNode,把原先p.next赋给x的next
TreeNode x = map.newTreeNode(h, k, v, xpn);
// 根据dir决定新节点应该是p的左节点还是右节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// p.next指向新节点
xp.next = x;
// 新节点的父节点以及prev指向p
x.parent = x.prev = xp;
// 如果原先p.next(新节点的next)不为null,将其指向新节点
if (xpn != null)
((TreeNode)xpn).prev = x;
// 重排红黑树使之平棚,并将root移到哈希桶的首元素
moveRootToFront(tab, balanceInsertion(root, x));
// 返回null,表示新插入元素,并非找到可以替换的元素
return null;
}
}
}
final TreeNode root() {
for (TreeNode r = this, p;;) {
// 不停寻找parent,直到parent==null,因为root.parent=null
if ((p = r.parent) == null)
return r;
r = p;
}
}
putTreeVal
是红黑树版的putVal,核心是利用了红黑树本质上是一棵二叉查找树的本质,不断比较当前节点的hash和需要插入元素的hash从而确定插入元素在左树还是右树。产生Hash碰撞主要由于两个原因:1)不佳的equals方法和hashCode方法,或者只重写了其中的一个,产生equals方法和hashCode产生不一致的情况(Bad coding practice);2)hashCode本身已经存在漏洞(Compromised hash algorithm)。其余逻辑在 treeify
和 balanceInsertion
都已经或多或少涉及。
本篇继续介绍了HashMap在Java8中的红黑树实现,通过对如何将链表转化为红黑树进而分析了Java8如何实现平衡红黑树,最后回到上一篇遗留的问题,当哈希桶中的首元素是TreeNode时,怎么将元素插入树结构。经过了2篇博客的分析,大致介绍了Java8对hashmap的底层实现,聚焦在平时最常用的put方法,当然HashMap还有许多我们平时也经常用的方法,由于时间有限,将不展开分析了,其原理万变不离其宗都是基于对hash的映射–>决定桶的位置–>并根据数据结构(链表或红黑树)进行增删改查,而时间复杂度都依据underlying的数据结构。
以上
© 著作权归作者所有
引用
[1] 红黑树(一)之 原理和算法详细介绍 http://www.cnblogs.com/skywang12345/p/3245399.html
[2] 源码分析之HashMap的红黑树实现, https://www.jianshu.com/p/5b157d4be1ad