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对象中。
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node 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
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。
没有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[] tab; Node 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 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)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,接下来则有几个分支:
若找到的e节点不为空,则将该节点的value覆盖,返回oldvalue。若容量大于阈值,则调用resize方法,afterNodeInsertion用于linkedhashmap,以后再看。接着我们来看一下resize的策略。
final Node[] resize() {
Node[] 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[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
}
完成对老表的容量和阈值设置后,则将当前阈值设置为新表的阈值。接着就需要将老表的节点赋值给新表了,若老表不为空,则遍历老表中的每个链表的头结点,大致流程跟put的过程相似:
返回新表,再次借用大佬博客中的图来解释为什么扩容后的头节点索引要么不变,要么就为(oldIndex_oldcap):
看完了put,再来看看get方法:
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以看到大概的逻辑,根据key的hash码和key值去找节点e,若节点e为空,则返回空,否则返回e.value,具体的getNode方法如下:
final Node getNode(int hash, Object key) {
Node[] tab; Node 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)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值对应的链表不为空(头节点不为空),则:
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
直接调到removeNode方法:
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node 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)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)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使用的,以后再看。
上面的介绍涉及到TreeNode相关的方法都只做了简单介绍,接着来仔细看看Hashmap中对红黑树的操作:
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;
do {
TreeNode 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源码如下:
final void treeify(Node[] tab) {
TreeNode root = null;
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)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 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 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节点调整为红黑树的根节点。
static TreeNode balanceInsertion(TreeNode root,
TreeNode x) {
x.red = true;
for (TreeNode 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个特性:
这里需要关注的是,在红黑树做了旋转操作之后,table中存储的头节点不一定仍然为红黑树的root节点,因此,要使用moveRootToFront方法将root节点调整为头节点。
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
//将原头节点存储到first中
TreeNode first = (TreeNode)tab[index];
if (root != first) {
Node rn;
//将root设置为头节点
tab[index] = root;
//找到root节点的prev节点
TreeNode rp = root.prev;
if ((rn = root.next) != null)
//root节点的next节点不为空时,令rn.prev=rp,等同于链表的删除操作, 下面的不再赘述
((TreeNode)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 getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
若该TreeNode的父节点不为空,则调用root方法找到root节点,为空则说明该节点就是root节点,接着调用find方法:
final TreeNode find(int h, Object k, Class> kc) {
TreeNode p = this;
do {
int ph, dir; K pk;
TreeNode 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位置的数据结构已经由链表转为红黑树了。
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;
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 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 xp = p;
//每次往下查找都会判断该节点是否为空,若为空,则说明到了叶子节点,并在该位置插入新的TreeNode节点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next;
TreeNode 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)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
前面的大半在前面已经介绍过,在插入TreeNode时,首先查找一遍,找到了就将该节点返回,若没找到则通过tieBreakOrder方法得到dir值,确定插入节点的方向,每次往下查找都会判断该节点是否为空,若为空,则说明到了叶子节点,并在该位置插入新的TreeNode节点,并调用balanceInsertion方法和moveRootToFront方法以保持红黑树的平衡与正常。接着来看看红黑树在扩容过程中从老表到新表中的迁移问题了,从resize方中可以看到,在遍历table中的头节点时,若该节点为TreeNode类型,则调用split方法进行红黑树的迁移。
final void split(HashMap map, Node[] tab, int index, int bit) {
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)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方法如下:
final Node untreeify(HashMap map) {
Node hd = null, tl = null;
for (Node q = this; q != null; q = q.next) {
Node p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
该方法将TreeNode转为Node类型。最后来看看树节点的删除。
final void removeTreeNode(HashMap map, Node[] 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 first = (TreeNode) tab[index], root = first, rl;
// 该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
// 则此处next即为node的next节点, prev即为node的prev节点
TreeNode succ = (TreeNode) 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 p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) { // node的左节点和右节点都不为空时
TreeNode 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 sr = s.right; // s的右节点
TreeNode pp = p.parent; // p的父节点
// 第一次调整start
if (s == pr) { // 如果p节点的右节点即为叶子节点
p.parent = s; // 将p的父节点赋值为s
s.right = p; // 将s的右节点赋值为p
} else {
TreeNode 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 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 r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // 如果p节点为叶子节点, 则简单的将p节点去除即可
TreeNode 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对该问题的解决方案,写博客的目的不是为了抄袭,而是跟着厉害的人学习,把这个学习的过程写下来,记在心里。