文章目录
前言
众所周知,jdk8的HashMap的链表会在一定长度转换为红黑树,且很多方法也与1.7有了很大变化。一、红黑树
规则:
- 根节点为黑;
- 叶子结点为黑色【叶子结点指的是Null节点】
- 插入的节点默认为红色
- 红色节点的子节点必为黑色;
- 从其中某一个节点开始,到其最底层节点【NULL节点】的路径上的黑色节点数目都是相同的
保证红黑树良好的插入查找特性的最重要的一条规则时最后一条。
1.1 插入规则
总的概括起来来说只有三条规则,牢牢记住就好:
需要注意的是:一、三是等价的。
插入的调整是递归的,永远优先考虑插入节点的祖父子三代,调整成功之后再去调整上面的节点,因此往往需要parent指针,向上递归。
-
若父节点为黑色,直接插入【因为默认插入的是红色,且红色会带来一个黑色的null节点,那么这条路径上的黑色节点数目是不变的】
-
若父节点为红色,且叔叔节点为黑色节点或者空:【为null或者黑色是等价的,同样处理即可】
此时优先选择变色:
- 将父节点变为黑色
因为父节点为红,祖父节点必不为红,因此调整叔叔节点即可。因为父节点变成黑色,此时这条路径就会多一个黑色,调整叔叔节点为黑,就可以解决祖父节点以下的问题,但是祖父节点以下的所有路径都会多一个黑色节点,需要另外做调整。
祖父节点若为根节点,就没有问题了:
父亲和叔叔变色,因为祖父为根节点,多两个黑色节点没有影响:
而假设祖父节点上面还有节点,乍一看叔叔为黑色怎么也不行的:
叔叔那边必然比父亲这边多一个黑色节点,无法达到平衡。
其实这种情况一般情况确实不会出现,在调整完成之后不可能出现,但是在调整过程中可能出现,如:父亲、叔叔都为红色的插入:
此时按照优先变色原则,父亲和叔叔都变成黑色,可以达到祖父以下这条路径的平衡。
但是对于祖父以上的节点,祖父以下的所有路径就会多出一个黑色节点,优先变色原则,将祖父变成红色:
此时,若是吧祖父当做新插入的节点,就会出现祖父的叔叔为黑色,父亲为红色的情况
此时,因为祖父的叔叔节点的路径已经不能调整,我们只能选择旋转:【分为左旋和右旋,其实都是一样的,按照左旋来想象:】《以下称呼都是对于祖父节点而言的》
将父节点作为根节点,将父节点的左孩子作为祖父节点的右孩子,将祖父节点作为父节点的左孩子,父节点的右孩子不变。
此时,进行旋转后的颜色调整:
二、HashMap关于红黑树方法
2.1 TreeNode内部方法
先看一下TreeNode的数据结构:
不仅拥有左右孩子,还有一个prev节点,看注释是为了记录下个被删除的节点。
一个布尔类型的red,为true表示该节点为红,为false表示节点为黑。
这个类继承自LinkedHashMap.Entry类,插入时需要传入四个值。
1. balanceInsertion()
/**
* 红黑树插入节点后,需要重新平衡
* root 当前根节点
* x 新插入的节点
* 返回重新平衡后的根节点
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true; // 新插入的节点标为红色
/*
* 这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
* xp:当前节点的父节点、xpp:爷爷节点、xppl:左叔叔节点、xppr:右叔叔节点
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 如果父节点为空、说明当前节点就是根节点,那么把当前节点标为黑色,返回当前节点
if ((xp = x.parent) == null) { // L1
x.red = false;
return x;
}
// 父节点不为空
// 如果父节点为黑色 或者 【(父节点为红色 但是 爷爷节点为空) -> 这种情况何时出现?】
else if (!xp.red || (xpp = xp.parent) == null) // L2
return root;
if (xp == (xppl = xpp.left)) { // 如果父节点是爷爷节点的左孩子 // L3
if ((xppr = xpp.right) != null && xppr.red) { // 如果右叔叔不为空 并且 为红色 // L3_1
xppr.red = false; // 右叔叔置为黑色
xp.red = false; // 父节点置为黑色
xpp.red = true; // 爷爷节点置为红色
x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
}
else { // 如果右叔叔为空 或者 为黑色 // L3_2
if (x == xp.right) { // 如果当前节点是父节点的右孩子 // L3_2_1
root = rotateLeft(root, x = xp); // 父节点左旋,见下文左旋方法解析
xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
}
if (xp != null) { // 如果父节点不为空 // L3_2_2
xp.red = false; // 父节点 置为黑色
if (xpp != null) { // 爷爷节点不为空
xpp.red = true; // 爷爷节点置为 红色
root = rotateRight(root, xpp); //爷爷节点右旋,见下文右旋方法解析
}
}
}
}
else { // 如果父节点是爷爷节点的右孩子 // L4
if (xppl != null && xppl.red) { // 如果左叔叔是红色 // L4_1
xppl.red = false; // 左叔叔置为 黑色
xp.red = false; // 父节点置为黑色
xpp.red = true; // 爷爷置为红色
x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
}
else { // 如果左叔叔为空或者是黑色 // L4_2
if (x == xp.left) { // 如果当前节点是个左孩子 // L4_2_1
root = rotateRight(root, x = xp); // 针对父节点做右旋,见下文右旋方法解析
xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
}
if (xp != null) { // 如果父节点不为空 // L4_2_4
xp.red = false; // 父节点置为黑色
if (xpp != null) { //如果爷爷节点不为空
xpp.red = true; // 爷爷节点置为红色
root = rotateLeft(root, xpp); // 针对爷爷节点做左旋
}
}
}
}
}
}
传入根节点的目的是为了检测性质,之后红黑树方法传入根节点root都是这个目的。
- 首先将插入节点x声名为红色
- 在for的开头定义了几个工作指针xp,xpp,xppl,xppr,表示x的父,祖父,叔【只可能为左/右一个】
- 若父节点为空,说明插入根节点,直接变色即可。
- 接下来是父节点不为空的逻辑:
-
若父节点为黑【插入规则1】或者祖父为空【父节点是根节点】,直接插入
-
若父节点为红色,且叔叔节点为红色【插入规则3】,将爷、父、叔分别变色,在递归地处理上面的节点。父亲和叔叔变为黑色,爷变成红色。将爷爷节点当做“新插入的节点”,考虑其爷、父、子三代的变化。
-
若父节点为黑色,叔叔节点为黑色或者空【插入规则2和 4】,需要分叔叔在左还是在右分别处理:
- 叔叔在父节点右边:
- 对x进行右旋【右旋的主角是x,x就能提上去代替父节点的位置】
- 父、爷染色
- 父节点右旋
- 父亲和叔叔置为黑色
- 在右:
- 叔叔在父节点右边:
-
【我在这里不对变量名进行改变是为了容易看出变化,实际上在程序运行过程中,会有变量指标随着位置的改变而改变,以达到“名如其实”的效果。
如:xpp = (xp = x.parent) == null ? null : xp.parent;
其次,看上去结果好像由“平衡”变得“不平衡”,实际上只有在真的不平衡的时候才有旋转,其实中间很多子节点都被省略掉了】
2. retateLeft()
左旋、右旋的主角就是移动上去的节点。
/**
* 节点左旋
* root 根节点
* p 要左旋的节点
*/
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
rl.parent = p; // 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
// 将要左旋的节点的右孩子的父节点 指向 要左旋的节点的父节点,相当于右孩子提升了一层,
// 此时如果父节点为空, 说明r 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p) // 如果父节点不为空 并且 要左旋的节点是个左孩子
pp.left = r; // 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
else // 要左旋的节点是个右孩子
pp.right = r;
r.left = p; // 要左旋的节点 作为 他的右孩子的左节点
p.parent = r; // 要左旋的节点的右孩子 作为 他的父节点
}
return root; // 返回根节点
}
【像rl = p.right = r.left 表示将l.left赋值给p.right, 并且为了以后表示更方便,用rl来标记这个节点【无论条件成不成立,这个赋值都会进行,因此在其他分支可以直接使用】】
设置几个工作指针,p表示要旋转的父节点,r表示p.right, rl表示r.left。如下图
第一步,将rl赋值为p.right,即找儿子
第二步,将rl的父亲赋值p,即认父亲【前提是rl不为null,否则NPE】
如下图:【暂时不考虑颜色】
接下来的几个if - else的图不能看上面的!
- if :pp = r.parent = p.parent
别管中间的,只是单纯的赋值而已。pp = p.parent == null,即p就是根节点了此时确保根节点为黑色即可,子节点什么颜色都行。
现在考虑 r.parent = p.parent的意思,即r会认p的父亲为父亲【认父亲】
剩下都表示p不是根节点的情况,pp是不是根节点没什么影响,因为接下来还是要递归处理pp:
- 若p是pp.left
if中的赋值使得r已经认了父亲,现在pp需要认r为儿子。r左旋上去,将p给替代了 - 若p是pp.right
一样的逻辑,只是把r作为pp的右孩子。
最后,将p作为r的左孩子。【找儿子】
再将p的父亲记作r【认父亲】
如下图的情况:
很奇怪:好像转了以后更不平衡了,其实并不是,因为我们从未考虑r.right有什么东西,实际上肯定是r.left或者r.right插入了黑节点才会引发左旋,在这之前r应该是平衡的,因此r.right也有东西。
另一种,p为pp左子树的情况:
在所有操作中,只有p为根节点时候考虑到颜色变化,难道左旋不必考虑颜色吗?
3. retateRight()
/**
* 节点右旋
* root 根节点
* p 要右旋的节点
*/
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
lr.parent = p; // 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
// 将要右旋的节点的左孩子的父节点 指向 要右旋的节点的父节点,相当于左孩子提升了一层,
// 此时如果父节点为空, 说明l 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p) // 如果父节点不为空 并且 要右旋的节点是个右孩子
pp.right = l; // 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
else // 要右旋的节点是个左孩子
pp.left = l; // 同上
l.right = p; // 要右旋的节点 作为 他左孩子的右节点
p.parent = l; // 要右旋的节点的父节点 指向 他的左孩子
}
return root;
}
右旋也是一个道理,只画图了:
前两个if的变化:
若p为根节点,直接染为黑色。
若不是,将l右旋上去,替代p的位置:
三、Map类的方法
HashMap内部节点仍然是Entry,而封装Entry对象的节点有两种:
链表节点Node类,红黑树节点TreeNode类。
因为这种性质,你会经常看到每个方法的逻辑是这样的:
(1)得到首个节点,若他的next为空,就不管是Node还是TreeNode都是一样的做法;
(2)反之,测算他的类型,对其做不同的处理:
TreeNode一般都是封装到一个方法里;
Node就在这个方法就地解决。
1. hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
不再出现大量移位,因为不需要那么复杂了.
h向右移动16位,是为了通过上位hashCode来获取index而不是最末的几位。
2. 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;
// 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
put()就是调用一下putVal()
-
定义几个临时变量:
tab, 表示HashMap的table数组,类型为Node[],保存每个链表/树的头结点。
p,工作指针
n 记录表长tab.length
i 记录当前操作的tab下标。 -
判断table有没有初始化,没有则调用resize()方法进行初始化
resize()会在后面进行说明,这个方法兼顾扩容和初始化的任务。 -
判断tab的对应当前key的hash下标位置是否为空,为空则直接插入新节点。
-
若不为空,进行链表或者红黑树的插入位置查找:
定义几个变量,
e:存储要插入的节点位置,用于外部的替换。
k:插入节点的key
若当前节点就已经达到要求,直接将当前节点赋值给p结束。【这里就是为什么是9个作为树化起点的坑。】
若不是,根据节点类型进行迭代:
若p为TreeNode,调用putTreeVal()进行查找,将最终位置赋值给e【这个方法会在后面说明】
若p为链表节点:
定义一个binCount记录查找的链表节点的个数,一直遍历到尾结点,并不断将当前节点赋值给e;
此时将新节点插入到尾结点的后面。
判断此时的binCount是否>=7,若大于等于7则进行树化。
treeifyBin()方法会在后面讲解。
这里有两个注意点,面试可能问到:
(1)1.8的链表插入使用尾插法。
(2)链表节点为9个时,进行树化:
最开始的p就已经是第二个节点了【查看刚开头检查key符不符合那个逻辑】。然后遍历了7趟,都不能满足6 >= 8 - 1【从0到6是七趟】, 此时节点数为8,
第8趟时,再次插入一个新的节点,此时节点数为9,但是binCount == 7,此时进行树化
-
若还未到尾结点,就找到了与目标key相等的节点,将此时的e跳出循环。
-
因为工作指针一直是p.next, 并在循环中不断将p.next赋值给e,因此迭代使用
p = e
-
所有情况都考虑上了,已经得到e了,开始进行替换逻辑:
若e不为null,进行替换:
记录下原值,替换并返回原值,modCount++; -
若所有node的数量之和 > 阈值,调用resize()进行扩容。
3. treeifyBin() & treeify()
/**
* 将链表节点转为红黑树节点
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 1.如果table为空或者table的长度小于64, 调用resize方法进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 2.根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// 3.将链表节点转红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
// 4.如果是第一次遍历,将头节点赋值给hd
if (tl == null) // tl为空代表为第一次循环
hd = p;
else {
// 5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
p.prev = tl; // 当前节点的prev属性设为上一个节点
tl.next = p; // 上一个节点的next属性设置为当前节点
}
// 6.将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
tl = p;
} while ((e = e.next) != null);
// 7.将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
/**
* 构建红黑树
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 1.将调用此方法的节点赋值给x,以x作为起点,开始进行遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next; // next赋值为x的下个节点
x.left = x.right = null; // 将x的左右节点设置为空
// 2.如果还没有根节点, 则将x设置为根节点
if (root == null) {
x.parent = null; // 根节点没有父节点
x.red = false; // 根节点必须为黑色
root = x; // 将x设置为根节点
}
else {
K k = x.key; // k赋值为x的key
int h = x.hash; // h赋值为x的hash值
Class<?> kc = null;
// 3.如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 4.如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
// 5.如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
else if (ph < h)
dir = 1;
// 6.走到这代表x的hash值和p的hash值相等,则比较key值
else if ((kc == null && // 6.1 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 6.2 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
// 7.dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 8.x和xp节点的属性设置
x.parent = xp; // x的父节点即为最后一次遍历的p节点
if (dir <= 0) // 如果时dir <= 0, 则代表x节点为父节点的左节点
xp.left = x;
else // 如果时dir > 0, 则代表x节点为父节点的右节点
xp.right = x;
// 9.进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 10.如果root节点不在table索引位置的头节点, 则将其调整为头节点
moveRootToFront(tab, root);
}
- treeifyBin()
-
定义几个变量:
n :tab.length, 查看是否需要扩容
index:当前table下标
e:工作指针,最终返回插入位置 -
并不会马上树化,优先选择扩容【只有table.length达到最大table的长度才进行树化】
为什么扩容可以代替树化?
扩容之后,表的散列型理论上会更好,因此链表的长度会缩短,此时冲突率也会降低,因此效率提升了
若节点不是很多,选择树化反而更加耗时。 -
遍历每个链表节点,将每个节点转为TreeNode,并将table[index]对应的头节点存储到hd中。
这里声明了两个变量:
hd:记录首节点;
tl:记录上一个节点,用于生成双向链表。
看看这个替换节点的方法:
很简单的方法,利用传入的原Node属性替换为TreeNode的属性即可。
- 对于第一个之后的节点,进行双向链表化:
将其prev指针指向前面,将next指针指向下一个
两个注意点:
TreeNode为什么会有next属性?
TreeNode继承自Entry类,拥有父类的next属性;prev与parent指针有什么区别?
prev指向的是链表的上一个节点,parent指的是树节点的父节点。
链表的上一个节点很有可能不是树节点的父节点,两者完全没有任何关系!
- tl代表下个节点的前节点,直接替换p即可。
- 将tab[index]的头指针关联到链表头结点,调用treeify()对整个table【index】进行树化。
双向链表示意图:
treeify():【将下标处的table[index]的TreeNode的双向链表转化为数组】
- root用来存储该树的根节点
- 遍历整个链表,工作指针使用x【实际插入的节点】,p【查找插入位置】
- 当root == null,表示当前是第一个节点,将其赋值给root【链表的第一个节点不一定是树根】
- 若root != null,表示当前在遍历之后的节点,需要根据比较来确定下个节点插入在左子树还是右子树。
声名的几个变量:
k: 当前节点的key
h:当前节点hash
kc:当前节点所属的类变量。【查看有无Comparable】
dir:表示比较结果,使用-1,0,1这种compare()式作为结果
ph:工作指针p的hash值
pk:p的key,用于第三轮比较
比较规则有四种:
①比较hash,这是TreeNode的一个属性,通过特有的hash()函数对原来的hashCode方法进行改造得来的【移位、异或】
②使用Comparable接口的compareTo()方法比较
comparableClassFor():
static Class<?> comparableClassFor(Object x) {
// 1.判断x是否实现了Comparable接口
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
// 2.校验x是否为String类型
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
// 3.遍历x实现的所有接口
for (int i = 0; i < ts.length; ++i) {
// 4.如果x实现了Comparable接口,则返回x的Class
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
comparableClassFor()方法:
- 判断x是否实现Comparable接口,若不符合,直接返回null;
- 若x为String,直接返回String.class
- 得到x实现的所有接口【getGenericInterfaces(), 得到Type[],表示得到的接口类型即泛型信息】
- 遍历Type[],若含有Comparable即返回x.class
- (ParameterizedType)t).getRawType()表示得到<>前面的内容。
- p.getActualTypeArguments()表示得到<>中间的内容
比较方式【续②】:
若得到的x.class不为空,表名实现了Comparable,调用其compareTo()方法,即compareComparables()
③tieBreakOrder(),包含两种比较方式:
(1)比较class的名称顺序【String的比较】;
(2)比较identitiHashCode(),这个方法表示通过Object类原生的hashCode()方法得到的哈希值,因为hashCode()方法很有可能被子类重写。
// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。
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;
}
几个易混属性的比较
这里给出我对这个解容易弄混的概念的分析:
(1)Node类的hash属性:
表示通过key的.hashCode方法经过高低位的异或来得到的,可以认为是当前K类的hashCode()的加工品。
(2)key:
key就是平时调用put()传入的第一个参数,通过调用hash()就能得到对应的node类的hash,用于插入table下标的计算【注意,即使key不相等,也有可能得到同样的key.hash(),也就可以插入相同的table数组的下标,因此两个节点插入到一条链表上不能说真的哈希冲突了,只能说这是node类的“hash冲突了”,究其原因hash()只使用其中的8位进行运算【若扩容会有所增长】】
(3)HashMap节点的hashCode():
有时候类会重写Object类的hashCode(),得到的结果往往用于equals()方法的比较。
Node类重写的hashCode()是将传入的key和value的值取hashCode()再异或。
(4)identityHashCode():即Object类原本的hashCode(),怎么计算的我也不知道,好像和内存地址有关,因为这个hashCode()基本不可能出现重复,因此称为“identity【识别身份使用的】HashCode”也就可以理解了。
-
上面已经提出了四种不同的比价方式,按照顺序,若不为0就不必进行后面几轮的比较了,现在获取到了dir值了
-
若dir <= 0, 将查找p的左节点,若dir > 0 查找p的右节点
使用xp记录父节点,用于插入x,xp会记录p最后一次不为null时候的值 -
调用红黑树的插入方法进行x的插入;
-
调整root的位置为table中头指针指向
在调整平衡树的过程中,可能因为旋转导致root节点偏移到了左子树或者右子树,因此要将table上原来的头指针改变指向。【这个方法看上去挺长。。。】
/**
* 将root放到头节点的位置
* 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
* 将root放到头节点的位置, 原头节点放在root的next节点上
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// 1.校验root是否为空、table是否为空、table的length是否大于0
if (root != null && tab != null && (n = tab.length) > 0) {
// 2.计算root节点的索引位置
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 3.如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
if (root != first) {
Node<K,V> rn;
// 3.1 将该索引位置的头节点赋值为root节点
tab[index] = root;
TreeNode<K,V> rp = root.prev; // root节点的上一个节点
// 3.2 和 3.3 两个操作是移除root节点的过程
// 3.2 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 3.3 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
if (rp != null)
rp.next = rn;
// 3.4 和 3.5 两个操作将first节点接到root节点后面
// 3.4 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
if (first != null)
first.prev = root;
// 3.5 将root节点的next属性设置为原头节点
root.next = first;
// 3.6 root此时已经被放到该位置的头节点位置,因此将prev属性设为空
root.prev = null;
}
// 4.检查树是否正常
assert checkInvariants(root);
}
}
这里面实际用上了之前的双向链表的next与prev指针,若root != first, 就查找到root的位置,替换原来first的位置。
最后使用断言查看树是否符合要求:【断言需要开启启动参数-ea
才能生效,因此此句可能不执行】
/**
* 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;
}
递归地对树的每个节点的指针进行检查,若一个节点指针不正确,程序就会异常停止。
4. resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1.老表的容量不为0,即老表不为空
if (oldCap > 0) {
// 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
// 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
else if (oldThr > 0)
newCap = oldThr;
else {
// 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e
oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
// 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 9.如果是普通的链表节点,则进行普通的重hash分布
Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> next;
do {
next = e.next;
// 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
// 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
// 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
// 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
// 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 12.返回新表
return newTab;
}
又是个罗里吧嗦的方法。
几个变量:
oldCap:原数组的容量【非常有用】
oldThr:原来阈值;
newCap:新容量
newThr:。。。
- 若oldCap > 0,表示原table不为空,才考虑扩容或者初始化
- 若oldCap大于最大的容量,将阈值拉大无穷大,表示已经无法扩容【之后无论怎么添加都打不到阈值,就不会触发resize()了】
- 将oldCap增加一倍,赋值给newCap,若仍不超过最大阈值,改变新的阈值【即将原来阈值 * 2】
- 若oldCap == 0,表示原table没有经过初始化,即要进行初始化的逻辑
- 设置cap和thr都为类的默认值
- 【为什么会出现默认阈值为0?】若thr == 0,使用
容量 * 负载因子
得到thr - 初始化新table,将算出来的属性赋值给他。
若oldTable不为空,说明这是扩容的逻辑【若是初始化逻辑,此时不会进入这个if分支】
- 进入循环,e为工作指针,初始化为对应下标j的头结点。
- 将原来的头结点的指针清空
- 若e.next == null,表示table[j]只有一个链表节点,直接结束【表还是树都不要紧】。
- 判断e节点的类型,若是树,调用split(后面讲)
- 若是链表节点,定义几个工作指针用于循环执行:
loHead:表示插入新链表下标为j的头结点;
loTail:表示插入新链表下标为j的尾结点;
hiTail:表示插入新链表下标为j + oldcap的尾结点;
hiHead:表示插入新链表下标为j + oldCap的头结点;
next:e的下个节点
老table与新table数组下标的呼应
这里说一下为什么要这么设计:
对于这种按照2的整数次幂计算的数组容量,且使用对应数位的hash码【如若表长为16,则采用4为hash计算下标】的计算方式,若扩容的新容量刚好为原来的两倍,则在原来的数组中位置为j的节点,在新送祝福中只可能有两种下标:
(1)若e.hash & oldCap == 0, 则j对应的新数组的下标也是j
(2)若e.hash & oldCap != 0,则j对应的新数组下标为j + oldCap
因为oldCap必为2的整数次幂,因此只有可能其中一个数位为0,因此与出来的结果只可能为0或者不为0
为什么会这样,我也不是很清楚。估计这是那些设计者之所以非要设计成2的整数倍的原因吧【快捷的位运算也是】
分析之后,后面的逻辑就很简单了。
若与出结果为0,将e挂到loHead为头结点的链表上;
若不为0,挂到hiHead为头结点的链表上,
之后直接将这两个链表挂到table的j和j + oldCap的位置。
最后,将尾结点的next设为null,并初始化链表对应位置,并设链表头结点为table[lo]和table[hi]的引用。
三种扩容的节点转移方式
到目前为止,已经有三种扩容的插入方法了,每种各有特点:
1.7的HashMap,采取一个节点结算一个新的index,算完之后深拷贝节点直接挂过去【采取头插法】;【转移次数 == size()】《需要new》
1.7的ConcurrentHashMap:采取一次转移多个相同index的相邻节点的方式:当数组i的位置出现一个节点e对应新数组的下标j的位置,他会检查这个e的相邻的几个子孙,将与其具有相同index = j的全部深拷贝,挂到一起在转移过去【转移次数不止常数次,且没有size()次】《需要new》
1.8的HashMap,采取分类的方式,设计只有两种可能链表头部作为头结点,将符合这个下标的一次挂到这个位置,这是修改指针的方式,不需要创建新节点【转移次数为常数次,即oldCap * 2次】《无需new》
5. split()
/**
* 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到调用此方法的节点
TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
int lc = 0, hc = 0;
// 1.以调用此方法的节点开始,遍历整个红黑树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历
next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点
e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收
// 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
++lc; // 统计原索引位置的节点个数
}
// 3.如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
++hc; // 统计索引位置为原索引+oldCap的节点个数
}
}
// 4.如果原索引位置的节点不为空
if (loHead != null) { // 原索引位置的节点不为空
// 4.1 如果节点个数<=6个则将红黑树转为链表结构
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 4.2 将原索引位置的节点设置为对应的头节点
tab[index] = loHead;
// 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (hiHead != null)
// 4.4 以loHead为根节点, 构建新的红黑树
loHead.treeify(tab);
}
}
// 5.如果索引位置为原索引+oldCap的节点不为空
if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空
// 5.1 如果节点个数<=6个则将红黑树转为链表结构
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点
tab[index + bit] = hiHead;
// 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (loHead != null)
// 5.4 以hiHead为根节点, 构建新的红黑树
hiHead.treeify(tab);
}
}
}
这个代码看上去很长,其实有很多重复的逻辑。
创建几个变量:
e :工作指针,初始化 e = b = this,表示调用的这个节点
lc:低下标的节点个数
hc:高下标的节点个数
剩下的几个是不是很熟悉?对,就是照抄之前的rehash()的做法【此处bit也表示oldTable的cap】
不同点在于,每次循环的过程中需要额外计算链表长度
为什么可以?为什么要转链表?
可以:TreeNode由于具有next和prev指针,因此可以说就是一个双向链表,具有线性和树形两种结构,也能使用链表的逻辑;
要转:对于节点数量低来说,插入、删除链表的速度可比红黑树快多了,因此若不超过阈值的话,优先选择分裂成链表
- 得到高低两个链表后,通过之前lc来判断是否达到转化红黑树的个数阈值,若未达到,解除该链表的树性质【untreeify()】,即链表化
/**
* 将红黑树节点转为链表节点, 当节点<=6个时会被触发
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点
// 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点
for (Node<K,V> q = this; q != null; q = q.next) {
// 2.调用replacementNode方法构建链表节点
Node<K,V> p = map.replacementNode(q, null);
// 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
if (tl == null)
hd = p;
// 4.否则, 将尾节点的next属性设置为当前节点p
else
tl.next = p;
tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点
}
// 6.返回转换后的链表的头节点
return hd;
}
逻辑很简单,按照原来的TreeNode隐藏的双向链表性质进行遍历,将每个TreeNode的属性取出来,并赋值给new的Node并替换,最后将头结点返回。
若大于阈值,需要转为红黑树。
对于hiHead对应的那半边数组也是一样的逻辑,只是下标需要从index转为index + bit
左边没有就不管了
6.get()与getNode()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1.对table进行校验:table不为空 && table长度大于0 &&
// table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6.找不到符合的返回空
return null;
}
get():
直接调用getNode(),传入key对应的hash()与key
getNode():
检查由该hash计算出来的节点的数组下标元素的hash是否符合传入地hash,符合进入此下标指向的链表/红黑树查找。
还是老套的逻辑:
先检查是不是只有一个节点或者首节点的key就是这个key;
在检查节点类型,若是TreeNode的节点,进入getTreeNode执行任务。
若是普通链表节点,进行迭代查找,未找到返回null。
7.getTreeNode()与find()
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 1.首先找到红黑树的根节点;2.使用根节点调用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 从调用此方法的节点开始查找, 通过hash值和key找到对应的节点
* 此方法是红黑树节点的查找, 红黑树是特殊的自平衡二叉查找树
* 平衡二叉查找树的特点:左节点<根节点<右节点
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 1.将p节点赋值为调用此方法的节点,即为红黑树根节点
TreeNode<K,V> p = this;
// 2.从p节点开始向下遍历
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 3.如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
if ((ph = p.hash) > h)
p = pl;
else if (ph < h) // 4.如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
p = pr;
// 5.如果传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null) // 6.p节点的左节点为空则将向右遍历
p = pr;
else if (pr == null) // 7.p节点的右节点为空则向左遍历
p = pl;
// 8.将p节点与k进行比较
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) && // 8.1 kc不为空代表k实现了Comparable
(dir = compareComparables(kc, k, pk)) != 0)// 8.2 k<pk则dir<0, k>pk则dir>0
// 8.3 k<pk则向左遍历(p赋值为p的左节点), 否则向右遍历
p = (dir < 0) ? pl : pr;
// 9.代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 10.代码走到此处代表“pr.find(h, k, kc)”为空, 因此直接向左遍历
else
p = pl;
} while (p != null);
return null;
}
直接调用find()方法, 传入的值为根节点【若parent == null,当前节点就是根节点;若不为null,调用root()查找根节点。】
root()是个逻辑简单的方法,一直向上迭代,直到没有parent的那个树节点返回。
find():
类似二叉搜索树的查找,不断比较左右节点的key与当前节点的k的大小,若小于进入左子树,大于进入右子树【二分查找】,若key == p.key或者k.equals(pk),直接返回p;
若无法直接比较,得到节点的类型若实现了Comparable就进行比较,若未实现,比较类名;还不能比较原生hashCode()
dir == 0,返回true。
等于null,返回null
8. remove()
/**
* 移除某个节点
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 3.否则将p.next赋值给e,向下遍历节点
// 3.1 如果p是TreeNode则调用红黑树的方法查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 3.2 否则,进行普通链表节点的查找
do {
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 赋值给node, 并跳出循环
break;
}
p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
} while ((e = e.next) != null); // e指向下一个节点
}
}
// 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 如果是TreeNode则调用红黑树的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
else if (node == p)
tab[index] = node.next;
// 4.3 否则将node的上一个节点的next属性设置为node的next节点,
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); // 供LinkedHashMap使用
// 5.返回被移除的节点
return node;
}
}
return null;
}
remove()分为两步,第一步查找到节点,和前面的get()一毛一样。
接下来进行节点删除【只有一个节点的情况在第一个逻辑中直接删除了】
TreeNode的删除使用额外的方法removeTreeNode,普通节点的删除直接修改next指针即可,注意若为头结点需要修改table的指针。
9. removeTreeNode()
/**
* 红黑树的节点移除
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// --- 链表的处理start ---
int n;
// 1.table为空或者length为0直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 2.根据hash计算出索引的位置
int index = (n - 1) & hash;
// 3.将索引位置的头节点赋值给first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
// 将node的next节点赋值给succ节点,prev节点赋值给pred节点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 5.如果pred节点为空,则代表要被移除的node节点为头节点,
// 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
if (pred == null)
tab[index] = first = succ;
else
// 6.否则将pred节点的next属性设置为succ节点(node的next节点)
pred.next = succ;
// 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
if (succ != null)
succ.prev = pred;
// 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
if (first == null)
return;
// 9.如果root的父节点不为空, 则将root赋值为根节点
if (root.parent != null)
root = root.root();
// 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
// (转链表后就无需再进行下面的红黑树处理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// --- 链表的处理end ---
// --- 以下代码为红黑树的处理 ---
// 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 12.如果p的左节点和右节点都不为空时
if (pl != null && pr != null) {
// 12.1 将s节点赋值为p的右节点
TreeNode<K,V> s = pr, sl;
// 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
while ((sl = s.left) != null)
s = sl;
// 12.3 交换p节点和s节点的颜色
boolean c = s.red; s.red = p.red; p.red = c;
TreeNode<K,V> sr = s.right; // s的右节点
TreeNode<K,V> pp = p.parent; // p的父节点
// --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
// 12.4 第一次调整
// 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
// 将sp赋值为s的父节点
TreeNode<K,V> sp = s.parent;
// 将p的父节点赋值为sp
if ((p.parent = sp) != null) {
// 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
if (s == sp.left)
sp.left = p;
// 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
else
sp.right = p;
}
// s的右节点赋值为p节点的右节点
if ((s.right = pr) != null)
// 如果pr不为空,则将pr的父节点赋值为s
pr.parent = s;
}
// 12.5 第二次调整
// 将p的左节点赋值为空,pl已经保存了该节点
p.left = null;
// 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
if ((p.right = sr) != null)
sr.parent = p;
// 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
if ((s.left = pl) != null)
pl.parent = s;
// 将s的父节点赋值为p的父节点pp
// 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
if ((s.parent = pp) == null)
root = s;
// 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
else if (p == pp.left)
pp.left = s;
// 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
else
pp.right = s;
// 12.6 寻找replacement节点,用来替换掉p节点
// 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
if (sr != null)
replacement = sr;
// 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
else
replacement = p;
}
// 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
else if (pl != null)
replacement = pl;
// 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
else if (pr != null)
replacement = pr;
// 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
else
replacement = p;
// 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
if (replacement != p) { // 如果p节点不是叶子节点
// 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
TreeNode<K,V> pp = replacement.parent = p.parent;
// 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
if (pp == null)
root = replacement;
// 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
else if (p == pp.left)
pp.left = replacement;
// 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
else
pp.right = replacement;
// 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 17.如果p节点不为红色则进行红黑树删除平衡调整
// (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 18.如果p节点为叶子节点, 则简单的将p节点去除即可
if (replacement == p) {
TreeNode<K,V> pp = p.parent;
// 18.1 将p的parent属性设置为空
p.parent = null;
if (pp != null) {
// 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
if (p == pp.left)
pp.left = null;
// 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
// 19.将root节点移到索引位置的头节点
moveRootToFront(tab, r);
}
红黑树的删除逻辑太过复杂,以后再看。
这里重点看一下链表化的条件:
与之前的扩容不同,不是判段节点的数量有没有到达阈值,而是看根节点是否存在、左右子树是否存在、左子树的左子树是否存在,左子树的左子树是否存在,任何条件只要不存在一个就退化为链表。
其实,若满足这些条件,节点数量必定不会太多(约10到13个以内)