HashMap是Java编程中常用的集合框架之一。
利用idea得到的类的继承关系图可以发现,HashMap继承了抽象类AbstractMap,并实现了Map接口(对于Serializable和Cloneable两个接口不去关心)。
下面进入主题:
1. 结构
HashMap的结构图如下:
源码中对应的成员为:transient Node<K,V>[] table;
是一个Node类型的数组,Node类型是HashMap中的一个静态内部类,并且还有的一个子类TreeNode,也是HashMap源码中比较重要的类类型。HashMap在存储时,为了避免一个链表过长,在操作时遍历链表会有较高的时间复杂度,因此规定,当同一下标下存储的元素个数小于8时,用链表,大于8时,用红黑树,一个树的节点个数小于6时,优惠转化为链表,具体的转化操作放在下面进行分析。
先看一下Node类型的源码:
//实现了Map接口中的Entry<K,V>接口
static class Node<K,V> implements Map.Entry<K,V> {
//只读的hash值
final int hash;
//键
final K key;
//值
V value;
//对下一个节点的引用
Node<K,V> next;
//构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//此类比较简单,对于其他方法这里不进行描述
...
}
Node类型中比较值得注意的是它保存了对下一个节点的引用,因此也通常称这种结构为“散列表”。
TreeNode类在HashMap中也叫做红黑树,因为它的节点被定义为红色和黑色两种。下面是TreeNode类部分源码:
//TreeNode类继承了LinkedHashMap.Entry<K,V>类,而LinkedHashMap.Entry<K,V>类则继承了HashMap.Node<K,V>类。
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//TreeNode类的源码过长,这里不占用过多篇幅,对于需要用到的方法会单独拿出来学习
...
}
对于前置节点,在刚开始阅读源码时,我是很迷茫的,为什么会有这个成员,是否有存在的必要性?在看到链表和树之间的转化时才明白原因,具体的解释也放在树和链表的相互转化中给出。
2. 键值对下表计算方式
HashMap中存储的是KV键值对,存储位置的计算方式:
//传入key,返回一个int类型的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(n - 1) & hash:将数组的长度减一与返回的hash值想与,计算出当前需要存储的键值对在数组中的下标;为什么会用数组的长度减一呢?
HashMap为了方便进行下标计算以及扩容等操作,数组的长度规定为2的n次幂。
最小的长度为 16:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最大长度在源码中定义为:static final int MAXIMUM_CAPACITY = 1 << 30;
如果不进行减一这步操作,那么&运算最后一位永远0,也就是说奇数位将无法存储,空间利用率将会大大降低,这点很容易理解。
3. 构造方法
在了解了存储结构和下标的计算方式之后,来看看构造方法,HashMap中给出了四种构造方法:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
通过构造方法可以发现,在初始化时并没有对Node数组进行初始化。而且对于大部分使用者来说,用的最多的便是无参构造。四种初始化方法中,都有loadFactor (加载因子)这个成员的赋值操作,可以看出这是一个十分重要的成员。
加载因子的作用在扩容的时候会体现出来,它决定了HashMap在什么时候进行扩容。我们最常用的无参构造,Node数组的默认大小为16,Node可以时列表也可以时红黑树,大大提高了存储的容量,但是面对数据量过大的情况,为了减少操作的时间,就不得不采取以空间换时间的做法,对整个表进行扩容。
HashMap定义了扩容的门限值threshold,门限值等于负载因子乘以表的容量。
4. 扩容方法
扩容方法是HashMap中的一个重要的方法,刚刚也提到了在构造方法中并没有对Node数组进行除始化操作,就是因为HashMap存在扩容的操作,在每次进行put的时候确认是否需要扩容无疑是非常好的选择,因此对于Node数组的初始化也就合并到了扩容的方法中,下面看看代码:
//返回值为Node类型的数组
final Node<K,V>[] resize() {
//transient Node<K,V>[] table;HashMap源码中的成员
Node<K,V>[] oldTab = table;
//旧表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧表的门限
int oldThr = threshold;
//定义新表的程度及门限
int newCap, newThr = 0;
if (oldCap > 0) {
//旧表的长度大于零,计算新表的容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//双参构造方法中有一条语句:this.threshold = tableSizeFor(initialCapacity);
//threshold成员定义时给出了明确的注释:field holds the initial array capacity
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化操作
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//计算新表的门限
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//设置门限为新表的门限
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧表不为null需要将旧表中的值复制到新表中
if (oldTab != null) {
//循环遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//链表长度为1的情况,可以直接计算其在新表中的下标并进行赋值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//红黑树的操作
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 保证有序性
//对于链表的操作 lo可以理解为low,hi理解为high,对于下面这段代码,咋一看觉得很难理解,解释起来也比较麻烦,解释写在代码结束后
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新表
return newTab;
}
解释:前面已经提到了HashMap下标的计算方式,对数组的长度减一再与key计算出来的hash值相遇,由于标长都是2的n次幂,减一之后最高位为零,这一步保证了下标计算时不会超过表长;但是,打个比方吧,假设表的长度为16,转化成二进制为(省略前面的0):1 0 0 0;减一之后为0 1 1 1
第一个key计算出来的hash值为(省略前面的位):0 0 0 1
第二个key计算出来的hash值为(省略前面的位):1 0 0 1
这两个key最后计算出来的下标值都为1;
扩容之后的表长为1 0 0 0 0,减1之后为: 0 1 1 1 1,计算出来的下表就不同了。
因此首先需要注意的是do…while循环中的第一个判断:(e.hash & oldCap) == 0,这不是进行下表计算的,是用来判断是否需要移动,下面的代码就很容易理解了,形成高低位两个链表;
其实需要注意,高位的下标计算方式:j + oldCap,当前位加旧表长度,也不难理解。
对红黑树的操作其实大体上与链表相同:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
红黑树在HashMap也是双向链表,对于链表和红黑树的相互转换在后面进行分析,这段代码不作多余的分析,对扩容操作的分析到这里也就告一段落了。
5. 链表和红黑树之间的转换
链表转化为红黑树:
//如果表太小,则进行扩容;否则将Node转换为TreeNode,并将链表转化为双向链表
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);
}
}
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;
//通过hash值判断插入位置
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)
//hash值相等需要判断键值决定插入到左右子树的哪一个
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);
}
这两段代码仔细去读并不难读懂,里面比较难的是平衡二叉树的代码。平衡二叉树是为了保证操作时的时间复杂度,如果大量的数据都集中在一棵子树上,那么深度过深必将影响速度。
到这里就需要了解一下左旋和右旋了:
左旋:
先看图了解一下什么叫左旋:
图中p和r是两个必须存在的节点,左旋也主要是针对这两个节点进行操作的。
下面结合图片分析一下代码
//传入的是根节点以及进行左旋操作的节点
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对应图中的A,r对应图中的C,rl对应图中的D;
//下面的判断理解起来比较绕:意思是当前操作的节点的右孩子节点有左孩子节点,将当前节点设置为其父节点,然后将当前节点设置为右孩子
的左孩子节点,并将其父节点设置为右孩子节点,结束。
if ((rl = p.right = r.left) != null)
rl.parent = p;
//当前节点没有右孩子节点且没有父节点,也就是说没有上图中的D,后面的操作也都建立在这个基础上
//当前节点为根节点,左旋后右孩子为根节点,设置为黑色;
//当前节点设置为右孩子节点的左孩子,并将右孩子节点设置为其父节点
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;
}
第一次if判断之所以没有判断当前节点是否有父节点的原因是,为真则将当前节点以及其左孩子节点都作为其右孩子节点的左孩子节点,因此没有必要关心其父节点。
右旋与左旋是相同的原理:
源码:
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;
}
理解了左旋之后,右旋相信就不难理解了。
到这里就必须了解一下红黑树的特性:
(1)每个节点要么是黑色,要么是红色。(节点非黑即红)
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
下面终于可以进行平衡二叉树的代码分析了:
了解红黑树的特性之后,根据其特性对其插入的情况做下分析:
1.插入的为根节点,则直接把颜色改成黑色即可。
2.插入的节点的父节点是黑色节点,则不需要调整,因为插入的节点会初始化为红色节点,红色节点是不会影响树的平衡的。
3.插入的节点的祖父节点为null,即插入的节点的父节点是根节点,直接插入即可(因为根节点肯定是黑色)。
4.插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的左节点。这种情况稍微麻烦一点,又分两种子情况:
i.插入节点的叔叔节点是红色,则将父亲节点和叔叔节点都改成黑色,然后祖父节点改成红色即可。
ii.插入节点的叔叔节点是黑色或不存在:
a.若插入节点是其父节点的右孩子,则将其父节点左旋,
b.若为左孩子,则将其父节点变成黑色节点,将其祖父节点变成红色节点,然后将其祖父节点右旋。
5.插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的右节点。这种情况跟上面是类似的,分两种子情况:
i.插入节点的叔叔节点是红色,则将父亲节点和叔叔节点都改成黑色,然后祖父节点改成红色即可。
ii.插入节点的叔叔节点是黑色或不存在:
a.若插入节点是其父节点的左孩子,则将其父节点右旋
b.若为右孩子,则将其父节点变成黑色节点,将其祖父节点变成红色节点,然后将其祖父节点左旋。
然后重复进行上述操作,直到变成1或2情况时则结束变换。
文字叙述理解起来比较难,看图吧:
下面是源码:
//传入的参数是根节点以及插入的新节点
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;;) {
//xp 当前节点的父节点
//xpp 当前节点的祖父节点
//xppl 当前节点的祖父节点的左孩子
//xppr 当前节点的祖父节点的右孩子
if ((xp = x.parent) == null) {
//父节点为null,则认为是根节点,设置为黑色
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
//父节点不是红色或者祖父节点为null(即父节点为根节点)
return root;
//父节点是祖父节点的左孩子
if (xp == (xppl = xpp.left))
//叔叔节点不为null,且为红色
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 {
//祖父节点的左孩子不为null,且为红色
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//祖父节点的左孩子为null,或为黑色
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);
}
}
}
}
}
}
这段代码理解起来很难,不仅分了很多种情况,而且还是在循环中处理,需要耐心下来慢慢阅读。
下面是我总结出来的一些规律:
- 只有在插入新的节点时,才需要平衡二叉树
- 插入的节点一定是叶子节点
- 平衡时最多只考虑到其祖父节点,因此每次只考虑其祖父节点以下的所有节点
- 通过循环使二叉树整体平衡
红黑树转化为链表:
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
6. put方法
put方法是HashMap中常用的方法之一,读懂了链表转化为红黑树的代码之后,put方法的源码读起来会很顺利。
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) // -1 for 1st
//链表转红黑树
treeifyBin(tab, hash);
break;
}
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;
}
其余的代码阅读起来难度不是很大,不再浪费篇幅。
7. 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;
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
这里面难度较大的应该是删除树节点的方法了:
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//先从双向链表中删除
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
//获取根节点
if (root.parent != null)
root = root.root();
//树转化为链表
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
//删除红黑树中的节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//左右孩子都不为空
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
//找出一个节点来代替待删除节点
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
//p是待删除节点,replacement用于后续的红黑树调整,指向的是p或者p的继承者。
//如果p是叶子节点,p==replacement,否则replacement为p的右子树中最左节点
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//若待删除的节点p时红色的,则树平衡未被破坏,无需进行调整。
//否则删除节点后需要进行调整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//p为叶子节点,则直接将p从树中清除
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
上面的代码非常的长,但是却没有多少难以理解的地方。
总结一下,上面的代码分为三种情况:
情景1:待删除的节点无左右孩子。
情景2:待删除的节点只有左孩子或者右孩子
情景3:待删除的节点既有左孩子又有右孩子。
对于情景1,直接删除即可;
对于情景2,则直接把该节点的父节点指向它的左孩子或者右孩子即可;
对于情景三则比较复杂,需要删除一个既有左孩子又有右孩子的节点时,需要考虑到一种最差的情况:红黑树的容量足够大,且待删除的节点是根节点。这种情形下最省时的办法就是找一个可以代替根节点的节点,这个节点是右子树的最左孩子或者左子树的最右孩子。
remove方法到这里就结束了,其他一些查找,遍历的方法就不在这里列举了。
关于HashMap源码阅读笔记也就此告一段落,文中如果有错误的理解希望路过的大佬可以给出指正。