从源码看容器-HashMap
1、构造器
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);
}
参数分别代表初始容量与负载因子,初始容量需小于MAXIMUM_CAPACITY(1<<30),负载因子默认为0.75。值得关注下的是tableSizeFor()这个方法,通过移位的方式来将threshod(hashmap大小临界值来初始化为2的整数次方):
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
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);
}
最后这种方式是通过传入一个map对象来创建hashmap对象,并将map中的元素拷贝到hashmap对象中。
2、节点(Node)
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node<k,v>为hashmap中的基础数据结构,键、值、散列码、链接的下一个节点,覆写了hashcode方法:key和value的hashcode按位异或而得;equals方法:instanceof(Map)&&(key和value的hash值都相等)。这里值得关注的是散列码hash的生成方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
不管增加、删除、查找键值对,定位到table中的索引位置都很关键,在hash的时候尽量使分布更为均匀,最好是每个位置只有一个元素,如上为生成hash码的方式,包括两步:1拿到key的hashCode值;2将hashCode的高位参与运算,重新计算hash值。而计算index时则是将计算出来的hash值与(table.length - 1)进行&运算,本质是用位运算替代模运算以达到模运算的效果,但是能够提高运算的速度,让高16位参与运算是为了使length较小时分布更为均匀,而且开销较小,示例流程如下:
该图来自https://blog.csdn.net/v123411739/article/details/78996181。
3、put
没有put就是空的,所以我们先从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;
}
可以看到node节点都是放在table中存储的,table存储的是一个链表数组。在构造函数中并没有去初始化这个table(除了最后一个),而是在第一次调用put时调用resize方法将table初始化为threshold大小。p = tab[i = (n - 1) & hash]这行是对null值做处理,开始已经说过table的容量为2的整数次方,那么n-1则为2的n次方减1,当key为null时,二者做与操作则为0,在hashmap中,将key为null的节点存储在index=0,即table[0]的链表中。通过传入节点的hash值找到对应的Index,并将这个链表的第一个节点赋值给p,接下来则有几个分支:
- 若节点p的hash值和key值与传入的节点值一样,则将p节点赋值给e。
- 若p节点为TreeNode类型,则调用putTreeVal()方法,后面介绍,查找该节点并赋值给e。
- 若跳入该分支,则说明该节点为普通节点,则遍历该链表找到该节点,若直到为空仍为找到该节点,则在该位置新建一个节点赋值给e,并判断该链表的长度是否大于等于8,若是,则将该链表转为红黑树。
若找到的e节点不为空,则将该节点的value覆盖,返回oldvalue。若容量大于阈值,则调用resize方法,afterNodeInsertion用于linkedhashmap,以后再看。接着我们来看一下resize的策略。
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;
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
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;
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)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
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;
}
- 这个方法代码较长,但思路很清晰,resize策略如下:
- 若现有table容量超过最大容量,则将容量设置为Integer.MAX_VALUE;
- 若老表容量小于最大容量且大于16,则将老的阈值扩容为原来的两倍;
- 老表容量为0,则更新为老表的阈值;
- 若老表的容量与阈值都为0,则设置为初始容量和阈值;
- 若新表的阈值为0,则通过新的容量×负载因子得到新的阈值
完成对老表的容量和阈值设置后,则将当前阈值设置为新表的阈值。接着就需要将老表的节点赋值给新表了,若老表不为空,则遍历老表中的每个链表的头结点,大致流程跟put的过程相似:
- 将头结点赋值给节点e,并将老表中的原节点置为空以便回收,若e.next为空,则说明该链表只有一个节点,在新表中计算e的index,并直接将e放在该位置;
- 若e为TreeNode类型,则调用split方法,将旧表中的树节点拷贝并释放,其方式类似与下面这个步骤。
- 对于普通节点,若(e.hash & oldCap) == 0,则表示扩容后的新表的索引位置与老表一样,则可以直接将老表中的链表赋值给e节点所在的链表。否则将该链表放到新表中Index为(oldIndex+oldcap)的位置上。
返回新表,再次借用大佬博客中的图来解释为什么扩容后的头节点索引要么不变,要么就为(oldIndex_oldcap):
5、get
看完了put,再来看看get方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以看到大概的逻辑,根据key的hash码和key值去找节点e,若节点e为空,则返回空,否则返回e.value,具体的getNode方法如下:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
若table不为空&&table的长度大于0&&key值对应的链表不为空(头节点不为空),则:
- 根据key的hash值找到对应的头结点,若头结点的key与传入的key是同一个object对象或二者的hashcode相等,则直接返回头结点;
- 若头节点是TreeNode类型,则调用getTreeNode方法在红黑树中查找对应节点并返回其value值;
- 若该节点是普通的链表节点,则判断hash值和key是否相等(或相同)来找到对应的节点;
- 若上述三个步骤都没找到,则返回空。
6、remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
直接调到removeNode方法:
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;
}
首先找到要删除的节点,这个过程前面都有说,不赘述了,找到之后,对节点类型进行判断,若为树节点,则调用removeTreeNode方法进行删除,若为链表节点,则将该节点删除,后面的顶上,afterNodeRemoval是LinkedHashmap使用的,以后再看。
7、TreeNode
上面的介绍涉及到TreeNode相关的方法都只做了简单介绍,接着来仔细看看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);
}
}
其中,replacementTreeNode方法将节点e转成TreeNode类型,将头结点存储在hd中,并遍历链表将p插到最后,接着调用treeify构建以hd为root节点的红黑树,treeify源码如下:
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;
//当p的hash值大于h,则往左边找
if ((ph = p.hash) > h)
dir = -1;
//否则往右边查找
else if (ph < h)
dir = 1;
//当x和p的hash值相等时,则比较key值
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//根据dir值判断并往对应的方向查找
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;
}
}
}
}
//将root节点调整为头节点
moveRootToFront(tab, root);
}
通过遍历链表,根据节点的hash值将节点插入到红黑树,并通过balanceInsertion方法控制红黑树的平衡,最后通过moveRootToFront方法将root节点调整为红黑树的根节点。
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);
}
}
}
}
}
}
该方法通过变色或旋转来控制红黑树的平衡,过程可以去参考详细的红黑树操作,通过这个方法来保证红黑树的以下5个特性:
- 1. 节点是红色或黑色。
- 2. 根节点是黑色。
- 3 每个叶节点(NIL节点,空节点)是黑色的。
- 4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这里需要关注的是,在红黑树做了旋转操作之后,table中存储的头节点不一定仍然为红黑树的root节点,因此,要使用moveRootToFront方法将root节点调整为头节点。
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;
//将原头节点存储到first中
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
//将root设置为头节点
tab[index] = root;
//找到root节点的prev节点
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
//root节点的next节点不为空时,令rn.prev=rp,等同于链表的删除操作, 下面的不再赘述
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
//root节点的next节点不为空且first不为空,则令first为root的
first.prev = root;
root.next = first;
root.prev = null;
}
//判断红黑树是否正常
assert checkInvariants(root);
}
}
在checkInvariants方法中对红黑树的五个特性进行检查。接着来看在getNode方法中调用的getTreeNode方法:
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
若该TreeNode的父节点不为空,则调用root方法找到root节点,为空则说明该节点就是root节点,接着调用find方法:
find
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
可以看到,该过程跟treeify中插入红黑树的方式相似,基本原理即为二叉树的查找。接着来看在put方法中是如何插入TreeNode的,此时,该index位置的数据结构已经由链表转为红黑树了。
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;
//每次往下查找都会判断该节点是否为空,若为空,则说明到了叶子节点,并在该位置插入新的TreeNode节点
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;
}
}
}
前面的大半在前面已经介绍过,在插入TreeNode时,首先查找一遍,找到了就将该节点返回,若没找到则通过tieBreakOrder方法得到dir值,确定插入节点的方向,每次往下查找都会判断该节点是否为空,若为空,则说明到了叶子节点,并在该位置插入新的TreeNode节点,并调用balanceInsertion方法和moveRootToFront方法以保持红黑树的平衡与正常。接着来看看红黑树在扩容过程中从老表到新表中的迁移问题了,从resize方中可以看到,在遍历table中的头节点时,若该节点为TreeNode类型,则调用split方法进行红黑树的迁移。
split
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);
}
}
}
在split方法中,首先需要遍历一次红黑树,判断这些节点的索引在迁移后是否需要变化(不变或者+oldcap),并在新表中构建新的链表,并统计变化与不变化的节点个数,在遍历完后,当个数小于等于6时,则在新表中找到loHead或hiHead节点,将该头节点后面的节点转化为Node类型;若大于6,则重新将新的链表构建为红黑树,untreeify方法如下:
untreeify
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;
}
该方法将TreeNode转为Node类型。最后来看看树节点的删除。
removeTreeNode
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab, boolean movable) {
// 链表的处理start
int n;
if (tab == null || (n = tab.length) == 0) // table为空或者length为0直接返回
return;
int index = (n - 1) & hash; // 根据hash计算出索引的位置
// 索引位置的头结点赋值给first和root
TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;
// 该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
// 则此处next即为node的next节点, prev即为node的prev节点
TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;
if (pred == null) // 如果node节点的prev节点为空
// 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
tab[index] = first = succ;
else
// 否则将node的prev节点的next属性设置为succ节点(node的next节点)(链表的移除)
pred.next = succ;
if (succ != null) // 如果succ节点不为空
succ.prev = pred; // 则将succ的prev节点设置为pred, 与上面对应
if (first == null) // 如果此处first为空, 则代表该索引位置已经没有节点则直接返回
return;
// 如果root的父节点不为空, 则将root赋值为根结点
// (root在上面被赋值为索引位置的头结点, 索引位置的头节点并不一定为红黑树的根结点)
if (root.parent != null)
root = root.root();
// 通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
// (转链表后就无需再进行下面的红黑树处理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// 链表的处理end
// 以下代码为红黑树的处理, 上面的代码已经将链表的部分处理完成
// 上面已经说了this为要被移除的node节点,
// 将p赋值为node节点,pl赋值为node的左节点,pr赋值为node的右节点
TreeNode<K, V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) { // node的左节点和右节点都不为空时
TreeNode<K, V> s = pr, sl; // s节点赋值为node的右节点
while ((sl = s.left) != null)//向左一直查找,直到叶子节点,跳出循环时,s为叶子节点
s = sl;
boolean c = s.red;
s.red = p.red;
p.red = c; //交换p节点和s节点(叶子节点)的颜色
TreeNode<K, V> sr = s.right; // s的右节点
TreeNode<K, V> pp = p.parent; // p的父节点
// 第一次调整start
if (s == pr) { // 如果p节点的右节点即为叶子节点
p.parent = s; // 将p的父节点赋值为s
s.right = p; // 将s的右节点赋值为p
} else {
TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) { // 将p的父节点赋值为s的父节点, 如果sp不为空
if (s == sp.left) // 如果s节点为左节点
sp.left = p; // 则将s的父节点的左节点赋值为p节点
else // 如果s节点为右节点
sp.right = p; // 则将s的父节点的右节点赋值为p节点
}
if ((s.right = pr) != null) // s的右节点赋值为p节点的右节点
pr.parent = s; // p节点的右节点的父节点赋值为s
}
// 第二次调整start
p.left = null;
if ((p.right = sr) != null) // 将p节点的右节点赋值为s的右节点, 如果sr不为空
sr.parent = p; // 则将s右节点的父节点赋值为p节点
if ((s.left = pl) != null) // 将s节点的左节点赋值为p的左节点, 如果pl不为空
pl.parent = s; // 则将p左节点的父节点赋值为s节点
if ((s.parent = pp) == null) // 将s的父节点赋值为p的父节点pp, 如果pp为空
root = s; // 则p节点为root节点, 此时交换后s成为新的root节点
else if (p == pp.left) // 如果p不为root节点, 并且p是父节点的左节点
pp.left = s; // 将p父节点的左节点赋值为s节点
else // 如果p不为root节点, 并且p是父节点的右节点
pp.right = s; // 将p父节点的右节点赋值为s节点
if (sr != null)
replacement = sr; // 寻找replacement节点(用来替换掉p节点)
else
replacement = p; // 寻找replacement节点
} else if (pl != null) // 如果p的左节点不为空,右节点为空,replacement节点为p的左节点
replacement = pl;
else if (pr != null) // 如果p的右节点不为空,左节点为空,replacement节点为p的右节点
replacement = pr;
else // 如果p的左右节点都为空, 即p为叶子节点, 替换节点为p节点本身
replacement = p;
// 第三次调整start
if (replacement != p) { // 如果p节点不是叶子节点
//将replacement节点的父节点赋值为p节点的父节点, 同时赋值给pp节点
TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null) // 如果p节点没有父节点, 即p为root节点
root = replacement; // 则将root节点赋值为replacement节点即可
else if (p == pp.left) // 如果p节点不是root节点, 并且p节点为父节点的左节点
pp.left = replacement; // 则将p父节点的左节点赋值为替换节点
else // 如果p节点不是root节点, 并且p节点为父节点的右节点
pp.right = replacement; // 则将p父节点的右节点赋值为替换节点
// p节点的位置已经被完整的替换为替换节点, 将p节点清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 如果p节点不为红色则进行红黑树删除平衡调整
// (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // 如果p节点为叶子节点, 则简单的将p节点去除即可
TreeNode<K, V> pp = p.parent; // pp赋值为p节点的父节点
p.parent = null; // 将p的parent节点设置为空
if (pp != null) { // 如果p的父节点存在
if (p == pp.left) // 如果p节点为父节点的左节点
pp.left = null; // 则将父节点的左节点赋值为空
else if (p == pp.right) // 如果p节点为父节点的右节点
pp.right = null; // 则将父节点的右节点赋值为空
}
}
if (movable)
moveRootToFront(tab, r); // 将root节点移到索引位置的头结点
}
由于这个方法较长,为了看起来不那么吃力,将注释写到代码后面。
总结
再次感谢大神的帖子https://blog.csdn.net/v123411739/article/details/78996181,这里还介绍了hashmap在jdk1.8之前在调用get方法时的扩容过程中可能出现的死循环问题,以及jdk1.8对该问题的解决方案,写博客的目的不是为了抄袭,而是跟着厉害的人学习,把这个学习的过程写下来,记在心里。
上一篇: K8S kubeproxy转发分析