目录
核心:为什么当目标节点是红黑树时,置换节点的子右是替换目标节点的首选
概述
HashMap
是常用的键值对存储结构,一个键对应一个值,并且键允许为null
,值也允许为null
,但由于其哈希性质,键不可以重复,因此只有一个键可为null
。HashMap
无法保证有序,因此放入顺序与取出顺序无法一致,且线程不安全。
原理简述
已知JDK1.8版本的Hashmap
由桶,链表、红黑树共同组成,每个桶位存储的都是链表的头部节点,而链表存储的都是键的哈希值存在冲突的数据,冲突的发生导致了链表结构的生成,冲突越多,链表结构越长,考虑到查询效率的问题,当链表超过一定长度时会由链表转换成红黑树来降低查询的时间复杂度,同时若冲突随着数据的移除而减少后,长度小于一个阈值,则会由红黑树转换回链表结构来降低结构的复杂度和树的平衡成本。
新旧版本对比
版本 | JDK1.7 | JDK1.8 |
结构 | 桶,链表 | 桶,链表,红黑树 |
性能 | O(n) | O(logn) |
结构设计原理
继承关系
关系 | 能力 | 对象 |
implements | 接口方法 | Map |
implements | 克隆模式 | Cloneable |
implements | 序列化 | Serializable |
extend | 抽象方法 | AbstractMap |
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializabl
成员变量
// 初始化Node数组(桶)的长度,必须是2的幂次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// Node最大容量不能大于1<<30(即2的30次幂)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 构造函数未指定负载因子时,默认的加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当一条链表长度达到8时,链表会转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当红黑树元素小于6个时,转换为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转变成树之前,还会有一次判断,只有Node数组(桶)长度大于 64 才会发生转换
static final int MIN_TREEIFY_CAPACITY = 64;
// Node数组(桶)
transient Node<K,V>[] table;
// Entry的Set集合
transient Set<Map.Entry<K,V>> entrySet;
// Hashmap中存储实例的个数
transient int size;
// 凡是我们做的增删改都会引发modCount值的变化,跟版本控制功能类似,
// 可以理解成version,在特定的操作下需要对version进行检查,适用于Fail-Fast机制
transient int modCount;
// 扩容阈值 计算方式为: capacity * loadFactor
int threshold;
// 可自定义的负载因子,不过一般都是用系统自带的0.75
final float loadFactor;
核心:为什么负载因子设定为0.75?
负载因子loadFactor
用作扩容时容量阈值的调和参数,计算方式:
allNodes > (capacity * loadFacotr)
,即存储的节点总数大于某个数量级就会触发扩容,而负载因子设定在0.75是为了考虑结构在时间和空间的成本折中的一个平衡数值,负载因子设定较高,扩容放缓,红黑树长时间处于树枝较深的状态导致查询效率降低,但也会降低内存空间的开销。而负载因子设定较低,扩容频繁,降低了树高度提高了查询效率,但相应的内存开销也会变大。
核心:为什么树化的链表阈值是8?
主要来看官方给出的解释:红黑树占用的空间大小大约是链表的两倍,因此并非一开始就使用红黑树来解决哈希冲突的问题,即便冲突过载结构进化为红黑树,也会随着表的删除等操作致使冲突减少而将红黑树退化为链表。当键的哈希离散性较好时,红黑树使用的几率不大。即使在随机离散的情况下,容器中节点的分布频率遵循泊松分布
,并且由于负载均衡因子的设定,超过一定阈值会扩容重新作哈希分布计算,每个链表中哈希冲突的个数大于8的概率为0.00000006,对撞概率极低,因此链表的阈值设定为8是概率统计的结果。
以下为官方释义:
* Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
核心:为什么树退化的链表阈值是6?
当红黑树结构生成后,存在因冲突减少而使结构退化为链表的可能,已知结构跃迁的阈值是一个冲突链上有8个节点,那么8就是临界点,考虑冲突的数量可能在一段时间内徘徊于临界点,导致结构在链表和红黑树之间不断地转换,造成不必要的开销,因此退化的阈值要稍小于树化的阈值。
构造函数
默认构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
传入初始容量大小的构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
传入初始容量及负载因子的构造方法
核心:传入初始容量位运算的原理
已知 Hashmap
规定桶的长度必须是,当使用自定义初始容量时,为了满足规定大小需要通过计算来调整。例如容量自定义为10或15时,容器会调整为16,自定义为17或29时,容器调整为32。这种调整都是通过tableSizeFor
的位运算完成的。
源码如下所示,共有5次右移运算,5次或运算,目的是让含1的比特位移动后的比特位也是1(若不进行或运算,比特位右移,而原位置得高位补充可能是0,则未达到将低位全部填充为1的目的)。
公式 n |= n >>> x
解释为参与计算的比特位的数量是 2x
,致使除了最高位其余位都参与了或计算,得出结果为低位全是1的二进制数,而低位全是1,该数无法满足,所以还需在最后 n+1
,得到1个最小的比n大的。而 int n = cap - 1
是为了处理自定义的容量值本身就满足。
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);
}
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;
}
传入Map对象的构造方法
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
数据结构
链表(开环单向)
结构组成
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 数组哈希索引位置
final K key;// 键
V value;// 值
Node<K,V> next;// 下一节点
……
}
核心:死循环场景复原
发生场景:并发插入元素,导致扩容后链表闭环。
版本:JDK1.7
代码如下:
public V put(K key, V value){
......
int hash = hash(key.hashCode());
// 计算索引位置
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 该key不存在,则在桶中新加一个索引位置
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex){
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若节点数量超过阈值threshold,则要扩容
if (size++ >= threshold)
resize(2 * table.length);
}
void resize(int newCapacity){
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
// 创建新桶
Entry[] newTable = new Entry[newCapacity];
// 旧数据迁移
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable){
Entry[] src = table;
int newCapacity = newTable.length;
// 从旧的桶中遍历,遍历出来的都是链表的头部节点,依次迁移
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
// 原桶索引置为null,方便垃圾回收
src[j] = null;
// 扩容机制将原链表倒装致新桶位中
do {
// --------线程挂起,并发扩容时出现问题的代码块-------
// 拎出下一个节点
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
// --------线程挂起,并发扩容时出现问题的代码块-------
// -------倒装-------
e.next = newTable[i];
newTable[i] = e;
e = next;
// -------倒装-------
} while (e != null);
}
}
}
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
// 判断桶内是否有元素
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 如果并发导致了链表闭环,则这里就会进入死循环
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 判断key是否相同,相同则返回对应的值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
红黑树
结构组成
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left;// 左子树
TreeNode<K,V> right;// 右子树
TreeNode<K,V> prev; // 前节点,需要在删除后解除链接
boolean red;// 颜色属性
……
}
红黑树特性
保持树平衡的两种方式:变色、旋转(左旋、右旋)
性质1 | 节点是红色或黑色 |
性质2 | 根节点是黑色 |
性质3 | 每个叶节点(NIL节点,空节点)是黑色的 |
性质4 | 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点,允许连续的黑色节点) |
性质5 | 从任一节点到其每个叶子的路径上包含的黑色节点数量都相同 |
性质6 | 通过键的哈希值来判断,插入时是放入左子节点还是右子节点,节点从左到从哈希值依次递增 |
上面6条性质约束了以下红黑树的关键特性:
- 从根到叶子节点的最长路径,不会超过最短路径的两倍。
- 达到基本平衡,虽然无法绝对平衡,但依然可以保持高效(时间复杂度较链表仍是降低的)。
核心:如何做到最长路径不超过最短路径的两倍?
- 性质4决定了路径上不能有两个相连的红色节点,最长路径一定是红色点和黑色节点交替而成的。
- 性质5决定了所有路径上都有相同数目的黑色节点,这就表明了没有路径能多于其他任何路径两倍长。
最短最坏情况为全部黑色,数值取 shortBlack = a
。
最长最坏情况为黑色节点数大于红色节点数,且红色黑色交替,数值取 longBlack = b
,longRed = c
,由于所有路径黑色数量相同,可以同时满足 a = b
和 c < b
两个条件,因此可以得出结论 (b + c) < 2a
。
核心:插入情况及旋转触发条件
条件一 | 插入不能是根节点,否则自平衡 |
条件二 | 插入时父节点不可以是黑色,或插入时树的深度不能为2,否则自平衡 |
条件三 | 父节点是祖父节点的左子节点(此时不考虑当前节点是子左还是子右)
|
条件四 | 父节点是祖父节点的右子节点(此时不考虑当前节点是子左还是子右)
|
重点方法分析
put方法
方法逻辑梳理:
- 对键作哈希处理,降低冲突。
- 桶是空状态,则初始化扩容,懒加载机制。
- 计算桶的索引位置是否为空,不为空则发生了冲突,为空则设定插入值为链表头节点。
- 发生哈希冲突,判断是否是相同的键,相同则覆盖Value,不同则判断是否是红黑树节点,是则插入红黑树叶子节点上,不是红黑树结构则插入链表尾部并判断是否需要树化。
- 判断表内的容量大小是否超过阈值,超过则触发扩容。
public V put(K key, V value) {
// 先将key值做哈希运算
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 将key的hashCode和其本身右移16位后的二进制作异或计算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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 检查是否是空桶,空桶则要初始化扩容,懒加载机制
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2 通过hash计算桶的索引位置,该位置为空则未冲突,直接在该处创建新的链表头节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3 索引位置不为空,则哈希冲突,开始解决冲突
else {
Node<K,V> e; K k;
// 4 判断冲突节点p的key和hash值是否跟传入的相等,若相等则未冲突,直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 5 冲突节点的键和要插入的键不同,判断节点是否为红黑树,是则调用红黑树解决冲突
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 6 冲突节点的键和要插入的键不同,使用链表解决冲突,链表长度超过8要树化
else {
for (int binCount = 0; ; ++binCount) {
// 6.1 如果冲突节点的下一节点为空,直接插入到下一个节点(即尾部节点)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 6.2 是否转为红黑树,-1是因为循环是从冲突节点p的next开始的
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 6.3 如果e节点存在hash值和key值都与传入的相同,则e节点即要插入的键,上次插入时冲突已被处理过,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 6.4 变量改为下一节点继续循环
p = e;
}
}
// 7 如果e节点不为空,则代表要插入的键存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 7.1 onlyIfAbsent 是公共属性,如果是true, 则不改变已存在的值,或旧值是null则改变该值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 用于LinkedHashMap
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 8 如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
// 用于LinkedHashMap
afterNodeInsertion(evict);
return null;
}
核心:存储时键的哈希运算原理
(h = key.hashCode()) ^ (h >>> 16)
计算将键的哈希值,与哈希值右移了16位后的值,作了异或的运算,目的是为了与 (n - 1) & hash
搭配使用时降低哈希冲突,(n - 1) & hash
是为了计算桶的索引位置,已知n
是桶的长度,而初始化的 Hashmap
的桶默认长度时16,当计算 (16 - 1) & hash
结果只有低位是1,高位全部是0,0的与运算结果必然是0,这种情况下,索引计算结果更多依赖于低位的值,冲突的概率比较高,而键的哈希值作了向右移动16位的运算,让高位也参与到索引位置计算中,可以降低冲突的概率,而桶的长度设置为也是为了保证 (n - 1)
时低位全部是1,高位全部是0,索引 (n - 1) & hash
计算后的结果不会超过桶的长度。
putTreeVal方法
该方法是链表冲突长度超过8,并且已经有一颗红黑树的前提下,直接将值插入到红黑树当中。红黑树插入会同时维护原来的单向链表结构和属性,并非由链表打散后生成红黑树。
方法逻辑梳理:
- 寻根,从红黑树根部节点开始操作,找到合适的插入位置。
- 根据键的哈希值大小判定插入左支还是右支树节点,传入的键的哈希值小于比较节点的哈希值则从左查找,否则从右查找。
- 未实现Comparable接口或比较后相等,或键的class类名来比较大小也相等,则使用类
System.identityHashCode
比较哈希大小。 - 找到合适插入点,插入红黑树最叶子节点,并且插入到链表结构中。
- 此时树还未平衡,要做左旋或右旋以及节点变色保持树平衡。
- 此时桶位上的头节点不一定是红黑树的root节点,调整root节点到桶位的头节点上。
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;
// 1 查找根节点, 索引位置的头节点并不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 2 将根节点赋值给p节点,开始进行查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 3 插入的hash值小于p节点的hash值,将dir设为-1,代表向左查找树
if ((ph = p.hash) > h)
dir = -1;
// 4 插入的hash值大于p节点的hash值,将dir赋值为1,代表向右查找树
else if (ph < h)
dir = 1;
// 5 插入的key与其hash值等于p节点的key与hash值,则直接返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 6 如果键class对象为null 并且 (键对象是否未实现过Comparable 或 键对象实现了Comparable且与p节点的键对象类型相同进行compareTo比较是左查还是右查)
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 6.1 从p节点的左右子节点分别查找遍历,如果查找到插入键则返回
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 6.2 p节点下左右查询,查到了则返回已存在的插入键
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;
}
// 6.2 否则使用定义的一套规则来比较k和p节点的key的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
// 7 dir<=0 则向p左边查找,否则向p右边查找,如果为null,则代表该位置即可插入,否则进入下一个循环查找合适位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 查找插入节点的父节点的链表结构中的next节点
Node<K,V> xpn = xp.next;
// 8 创建新的节点, 插入节点放入xp和xpn中间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
// 9 根据dir是否<=0, 来设定x是xp的子左还是子右
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
// 10 如果xpn不为空,设置x为xpn的前节点
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 11 balanceInsertion进行树平衡操作
// 12 moveRootToFront调整root节点到桶位的头节点上
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final TreeNode<K,V> root() {
// 向上遍历获取根节点
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
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;
}
static int compareComparables(Class<?> kc, Object k, Object x) {
// kc:插入键class对象,k:插入键,x:当前树节点的键
// 如果当前树节点的键为null 或 x的class对象与kc类型相同,则返回0,代表不分左右查询
// 否则进行compareTo比较,返回左右方向
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// h:插入键hash值,k:插入键, kc:插入键class对象
// 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值,向左遍历
if ((ph = p.hash) > h)
p = pl;
// 4 插入的hash值大于p节点的hash值,向右遍历
else if (ph < h)
p = pr;
// 5 插入的key与其hash值等于p节点的key与hash值,直接返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 6 p节点的子左为空,向右遍历
else if (pl == null)
p = pr;
// 7 p节点的子右为空,向左遍历
else if (pr == null)
p = pl;
// 8 若比较的hash值相等,则根据comparable方式比较大小
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 9 若key所属类未实现Comparable, 直接指定向右遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 10 向右遍历为空, 改为向左遍历
else
p = pl;
} while (p != null);
return null;
}
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;
}
// 调整的不是红黑树里面的排列顺序,是维护的违双向链表的排列顺序,调整root到头节点
// 并判断头节点是否有前节点,有得话则移到第二位上
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;
// 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 如果原头节点不为空, 则将原头节点的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);
}
}
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;
}
treeifyBin方法
该方法是链表转红黑树方法,触发条件是当链表的插入冲突超出阈值8便树化。转化成红黑树后仍然维护链表关系,但是此时维护的链表成为了开环违双向链表,因为next指针正常,但是prev指针并不是线性的。
方法逻辑梳理:
- 判断一次桶是否为空和长度阈值,是否需要进行扩容。
- 循环链表上的每个节点,先构建出红黑树节点,并关联好前后节点指针,此时还未生成分支结构。
- 红黑树开始生成左右树枝,先确定根节点,后依次将next节点放入子左或子右,且每经历一个next节点处理,都进行一次平衡操作。
final void treeifyBin(Node<K,V>[] tab, int hash) {
// tab:桶,hash:插入键hash值
int n, index; Node<K,V> e;
// 1 判断一次桶是否为空,或长度小于64, 否则进行一次扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 2 计算桶的索引位置,从头节点开始遍历该条链表
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 若tl为null,代表初次循环,将头节点赋值给hd
if (tl == null)
hd = p;
// 5 否则关联好当前节点的前后节点
else {
p.prev = tl;
tl.next = p;
}
// 6 将p节点赋值给tl,用于下次循环关联操作
tl = p;
} while ((e = e.next) != null);
// 7 将创建好的红黑树的头节点放入到桶的索引位置上,若头节点不为空,开始构建左右分支
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 1 将调用此方法的节点为起点开始遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// 下个节点
next = (TreeNode<K,V>)x.next;
// 将x的左右节点设置为空
x.left = x.right = null;
// 2 若没有根节点, 将x设为根节点
if (root == null) {
// 根节点没有parent节点
x.parent = null;
// 将根节点置黑(特性)
x.red = false;
// 将x设为根节点
root = x;
}
else {
// 去除x节点的key和hash
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 3 从根节点开始遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 4 如果x节点的hash值小于p节点的hash值,向左查找
if ((ph = p.hash) > h)
dir = -1;
// 5 如果x节点的hash值大于p节点的hash值,向右查找
else if (ph < h)
dir = 1;
// 6 比较后若hash值相等,则通过Comparable比较key值
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 7 dir<=0 则向左查找,否则右查找,若为null代表该位置可插入,否则继续循环,p是变化的next节点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// x的父节点即为最后一次遍历的p节点
x.parent = xp;
// 如果时dir <= 0, 成为子左
if (dir <= 0)
xp.left = x;
// 如果时dir > 0, 成为子右
else
xp.right = x;
// 9 执行一次红黑树平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
// 10 调整root节点到桶位的头节点上
moveRootToFront(tab, root);
}
balanceInsertion方法
方法逻辑梳理:
- 插入节点x若没有父节点,则证明是根节点,置黑返回根节点。
- 插入节点x的父节点是黑色,代表当前平衡,直接返回,若其祖父节点为空,代表当前是第二层,直接返回根节点。
- 插入节点x的父节点是祖父节点的子左:
- 右叔节点非空且呈红色,则叔、父节点置黑,祖父节点置红,起点x改为祖父节点重新开始循环。
- 右叔节点为空或呈黑色,呈黑色代表平衡正在运行中。
- 插入节点是子右,则父节点左旋(父节点下降,当前节点和父节点换了位置,且原父节点成为了子左)。
- 父节点不为空则父节点置黑,祖父节点不为空则祖父节点置红,祖父节点右旋(祖父节点下降)。
- 插入节点x的父节点是祖父节点的子左:
- 左叔节点非空且呈红色,则叔、父节点置黑,祖父节点置红,起点x改为祖父节点重新开始循环。
- 左叔节点为空或呈黑色,呈黑色代表平衡正在运行中。
- 插入节点是子左,则父节点右旋(父节点下降,插入节点和父节点换了位置,且原父节点成为了子右)。
- 父节点不为空则父节点置黑,祖父节点不为空则祖父节点置红,祖父节点左旋(祖父节点下降)。
// 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;;) {
// 条件1:父节点是nil,说明插入节点就是根节点,置黑,直接返回根节点
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 条件2:父节点是黑色证明当前是平衡状态,置红,直接返回根节点,或祖父节点是nil,证明插入节点是第二层,直接返回根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 条件3:父节点是祖父节点的子左
if (xp == (xppl = xpp.left)) {
// 条件3.1:右叔节点非nil且呈红色,则叔、父节点置黑,祖父节点置红,起点改为祖父节点
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 条件3.2:右叔节点是nil或呈黑色,呈黑色代表平衡正在运行中
else {
// 条件3.2.1:插入节点是子右,则父节点左旋(父节点下降,当前节点和父节点换了位置,且原父节点成为了子左)
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 条件3.2.2:父节点不为nil则父节点置黑,祖父节点不为nil则祖父节点置红,祖父节点右旋(祖父节点下降)
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
// 条件4:父节点是祖父节点的子左
else {
// 条件4.1:左叔节点非nil且呈红色,则叔、父节点置黑,祖父节点置红,起点改为祖父节点
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 条件4.2:左叔节点是nil或呈黑色,呈黑色代表平衡正在运行中
else {
// 条件4.2.1:插入节点是子左,则父节点右旋(父节点下降,插入节点和父节点换了位置,且原父节点成为了子右)
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 条件4.2.2:父节点不为nil则父节点置黑,祖父节点不为nil则祖父节点置红,祖父节点左旋(祖父节点下降)
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
rotateLeft方法
该方法是红黑树平衡的核心方法,将树结构向左旋转。
这里旋转只分为两种不同情况:
- 情况一:旋转节点的子右的子左是空。
- 情况二:旋转节点的子右的子左非空。
else if (pp.left == p)该判断仅是旋转节点的父节点变了左指针或是右指针,不是关键,因此不纳入情况范围,同时if ((pp = r.parent = p.parent) == null)这个判断仅影响了父节点的颜色和是否为根,不是关键,因此也不纳入情况范围。
// root:根节点,p:要左旋的节点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
// 旋转节点与其子右非nil
if (p != null && (r = p.right) != null) {
// 旋转节点的子右的子左节点 成为 其子右,父认子
if ((rl = p.right = r.left) != null)
// 子认父
rl.parent = p;
// 旋转节点原子右 指向 其父节点,若父节点为nil,说明r已经root节点并置黑
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
// 旋转节点是子左,其父节点的子左指向r
else if (pp.left == p)
pp.left = r;
// 旋转节点是子右,其父节点的子右指向r
else
pp.right = r;
// 旋转节点成为其原子右的子左
r.left = p;
// 旋转节点原子右成为其父节点
p.parent = r;
}
return root;
}
核心:左旋情况一演示
核心:左旋情况二演示
rotateRight方法
该方法是红黑树平衡的核心方法,将树结构向右旋转。
这里旋转只分为两种不同情况:
- 旋转节点的子左的子右是空。
- 旋转节点的子左的子右非空。
else if (pp.right== p)该判断仅是旋转节点的父节点变了左指针或是右指针,不是关键,因此不纳入情况范围,同时if ((pp = l.parent = p.parent) == null)这个判断仅影响了父节点的颜色和是否为根,不是关键,因此也不纳入情况范围。
// root:根节点,p:要右旋的节点
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
// 旋转节点与其子左不为nil
if (p != null && (l = p.left) != null) {
// 旋转节点的子左的子右 成为 其子左,父认子
if ((lr = p.left = l.right) != null)
// 子认父
lr.parent = p;
// 旋转节点原子左 指向 其父节点,若父节点为nil,说明r已经root节点并置黑
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
// 旋转节点是子右,其父节点的子右指向l
else if (pp.right == p)
pp.right = l;
// 旋转节点是子左,其父节点的子左指向l
else
pp.left = l;
// 旋转节点成为其原子右的子右
l.right = p;
// 旋转节点原子左成为其父节点
p.parent = l;
}
return root;
}
核心:右旋情况一演示
核心:右旋情况二演示
resize方法
扩容场景:
- 桶位为空时,初始化扩容
- 桶位被删光时,存放扩容
- 桶位长度到达阈值时,触发扩容
方法逻辑梳理:
- 根据扩容场景以及当前桶的长度,重新定义新桶的长度和新桶的扩容阈值。
- 数据从旧桶迁移到新桶:
- 旧桶不为空,且是链表结构,执行链表迁移:
- 重新计算节点索引位置。
- 链表节点指针方向不变迁移(JDK1.7版本链表迁移是倒装的),部分节点可能迁移后索引位不变,部分节点可能迁移后索引位发生变化,这就导致一条链表被打散了。
- 旧桶不为空,且是红黑树结构,执行红黑树迁移
- 重新计算节点索引位置。
- 仅变幻红黑树的链表结构迁移。
- 部分节点可能迁移后索引位不变,部分节点可能迁移后索引位发生变化,这就导致一条链表被打散,打散后的链表头节点都要重新构建红黑树。
- 旧桶不为空,且是链表结构,执行链表迁移:
// oldCap:旧桶长度
// oldThr:旧桶扩容阈值
// newCap:新桶长度
// newThr:新桶扩容阈值
// loHead:扩容后,索引位不发生变化的临时头节点
// loTail:扩容后,索引位不发生变化的临时尾节点
// hiHead:扩容后,索引位发生变化的临时头节点
// hiTail:扩容后,索引位发生变化的临时尾节点
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 旧桶不为空的情况
if (oldCap > 0) {
// 1.1 判断旧桶长度是否超过最大容量值,若超过直接设置为Integer.MAX_VALUE,不扩容,返回旧桶
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 将新桶长度设为旧桶长度的2倍,若新桶长度 < 系统最大容量且旧桶长度 >= 16, 则将新桶扩容阈值设为旧桶扩容阈值的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 2 若旧桶长度为0, 但旧桶扩容阈值大于0, 则将新桶扩容阈值设为旧桶的扩容阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 3 旧桶长度为0, 旧桶扩容阈值为0,则将阈值和长度设为系统默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4 若新桶扩容阈值为0, 则通过新桶长度 乘以 负载因子 获得新桶的扩容阈值,并判断是否超出系统默认最大容量
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 5 新桶的扩容阈值赋值给threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据新的容量,创建新桶
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 新桶赋值诶table
table = newTab;
// 6 若旧桶不为空,则遍历旧桶,将数据迁移到新桶
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 6.1 旧桶每个索引位的链表头节点,赋值给e
if ((e = oldTab[j]) != null) {
// 6.2 原旧桶的索引位置为null,方便垃圾回收
oldTab[j] = null;
// 7 如果e.next为空,则代表旧桶该链表只有一个节点,则计算新桶的索引位置,直接将该节点放在新桶的索引位上
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 8 若是红黑树节点,则进行红黑树的rehash
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 9 若是普通的链表,则进行链表的rehash
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;
// 9.1 若e的哈希值与旧桶容量作与运算为0,则e的新索引位置与其在旧桶的索引位置相同
if ((e.hash & oldCap) == 0) {
// 若loTail为空, 代表当前的e是头节点,则将loHead赋值为头节点,否则放到队尾
if (loTail == null)
loHead = e;
else
loTail.next = e;
// 将loTail赋值为新增的节点,方便loTail.next = e作下节点的指向
loTail = e;
}
// 9.2 若e的哈希值与旧桶容量作与运算非0,则e的新索引位置 = 其在旧桶索引位+旧桶长度
else {
// 若hiTail为空, 代表当前的e是头节点,则将hiHead赋值为头节点,否则放到队尾
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
// 将hiTail赋值为新增的节点,方便hiTail.next = e作下节点的指向
hiTail = e;
}
} while ((e = next) != null);
// 10 若loTail不为空,表明旧桶有索引位不变的节点放入到了新桶同样的索引位上,将loTail最后的指针指向null,并将loHead放入到新桶的对应索引位上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11 若hiTail不为空,标明旧桶的节点迁移到新桶的索引位为:原桶所在索引+旧桶长度,将hiTail最后的指针指向null,并将hiHead放入到新桶的计算后的新索引位上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 12 返回新桶
return newTab;
}
// 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置 + 旧桶长度
// map:当前的HashMap对象
// tab:新桶
// index:桶的索引位置
// bit:旧桶长度
// loHead:扩容后,索引位不发生变化的临时头节点
// loTail:扩容后,索引位不发生变化的临时尾节点
// hiHead:扩容后,索引位发生变化的临时头节点
// hiTail:扩容后,索引位发生变化的临时尾节点
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// this是调用此方法的红黑树节点
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;
// 1 从this节点开始遍历红黑树
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
// 旧桶节点设为null,方便垃圾回收
e.next = null;
// 2 若e的哈希值与旧桶容量作与运算为0,则e的新索引位置与其在旧桶的索引位置相同
if ((e.hash & bit) == 0) {
// 若loTail为空, 代表当前的e是头节点,则将loHead赋值为头节点,否则放到队尾
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
// 将loTail赋值为新增的节点,方便loTail.next = e作下节点的指向
loTail = e;
// 统计原索引位的节点个数
++lc;
}
// 3 若e的哈希值与旧桶容量作与运算非0,则e的新索引位置 = 其在旧桶索引位+旧桶长度
else {
// 若hiTail为空, 代表当前的e是头节点,则将hiHead赋值为头节点,否则放到队尾
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
// 将hiTail赋值为新增的节点,方便hiTail.next = e作下节点的指向
hiTail = e;
// 统计索引位为原索引位 + 旧桶长度的节点个数
++hc;
}
}
// 4若原索引位的节点不为空
if (loHead != null) {
// 4.1 若节点个数<=6个则将红黑树退化为链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 4.2 将loHead设为新桶对应索引位的头节点
tab[index] = loHead;
// 4.3 若hiHead不为空,则代表原来的红黑树发生了变化(一个链表上的分散到了不同链表上),需要重新构建红黑树
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 5 若索引位为原索引位 + 旧桶长度的节点不为空
if (hiHead != null) {
// 5.1 若节点个数<=6个则将红黑树退化为链表
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 5.2 将索引位为原索引 + 旧桶长度的节点设为新桶对应索引位的头节点
tab[index + bit] = hiHead;
// 5.3 若loHead不为空,则代表原来的红黑树发生了变化(一个链表上的分散到了不同链表上),需要重新构建红黑树
if (loHead != null)
hiHead.treeify(tab);
}
}
}
// 红黑树退化成链表
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
// 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属性设置为当前节点
else
tl.next = p;
// 5 每次都将tl节点指向当前节点,即尾节点
tl = p;
}
return hd;
}
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
核心:扩容时旧链表的节点计算新的索引位的计算原理
(e.hash & oldCap) == 0 是计算新索引位置的判定条件,解释为原节点的哈希值同旧桶的长度作与运算,旧数组的数组长度是 ,公式即为 (e.hash & ) == 0 , 表示为是一个仅有一个比特位是1的二进制数,其他位都是0。若旧桶长度 比特位上是1的位置,且节点哈希值对应这个位置上的值不是1,那么运算结果一定是0。此时出现两种情况:
- 运算结果等于0:为0时,则该节点迁移时的索引位等于其旧桶所在的索引位。
- 运算结果不为0:为1时,则该节点迁移时的索引位等于其旧桶所在的索引位再加上旧桶的长度。
而这样设计的原因:
一条链表上的节点的key哈希值不一定都相等,而是 (n - 2)& hash 经过原哈希值取模之后桶位相等。已知hash是定量,n - 1 是变量,例如在桶长度为16时,只有低4位同key的哈希值作了与运算,高位都置为了0,那么扩容之后,n - 1 发生变化,那结果就不一定等于原来计算出的索引位(除非hash值正好某高几位是0与原结果相同),下图展示一次相同链表上的节点扩容后拆分的场景。
根据扩容后新桶长度时旧桶长度的2倍,则 (n - 1) & hash 的变种为(2n - 1) & hash 。
- 以初始长度 n = 16 ,旧桶计算方式: (n - 1) & hash = 0000 0000 0000 1111 & hash
- 以初始长度 n = 16 ,新桶计算方式: (2n - 1) & hash = 0000 0000 0001 1111 & hash
那么计算 (n - 1) & hash 时,新旧桶的区别就在于第5位比特位是0还是1,扩容后 (n - 1) 的第5位是1,若同时 hash 的第5位也是1,则第5位计算结果刚好是旧桶长度,仅需要旧桶索引加上旧桶长度就可得出新桶的索引位,其他情景桶扩容同理。而 e.hash & oldCap 就是为了证明第5位的计算结果是否为1。
remove方法
方法逻辑梳理:
- 检查:若桶不为空且长度不为0,且入参hash值对应索引位不为空。
- 检查:查找要删除的目标节点:
- 节点为链表类型。
- 节点为红黑树类型。
- 若是链表结构,则剔除链表节点,处理指针。
- 若是红黑树结构:
- 解除链表结构中的关联,同时检查树的节点数,节点过少则将红黑树退化回链表结构。
- 检查:若目标节点的子左和子右都不为空时。
- 找到置换节点(默认置换节点是查找目标节点子右的最深子左),与其交换颜色。
- 若置换节点是目标节点的子右,则两节点地理位置互换。否则,将置换节点的父节点设为目标节点的父节点,同时若置换节点的父节点不为空,将目标节点替换置换节点设为其父节点的子节点,同时目标节点的子右设为置换节点的子右,若目标节点的子右不为空,则将置换节点设为目标节点子右的父节点。
- 将目标节点的子左设为空。
- 若置换节点的子右不为空,将置换节点的子右设为目标节点的子右,同时将目标节点设为置换节点子右的父节点。
- 若置换节点的子左不为空,将目标节点的子左设为置换节点的子左,同时将置换节点设为目标节点子左的父节点。
- 若目标节点的父节点为空,将目标节点的父节点设为置换节点的父节点,同时将置换节点设为root根。否则,若目标节点是其父节点的子左,则将置换节点设为目标节点父节点的子左。否则,若目标节点不是root,且是其父节点的子右,则将置换节点设为目标节点父节点的子右。
- 若置换节点的子右不为空,则赋值给replacement变量,否则将目标节点赋值给replacement变量。
- 检查:若目标节点的子左不为空,replacement 赋值为其子左。
- 检查:若目标节点的子右不为空,replacement 赋值为其子右。
- 检查:若目标节点的子左子右都为空,replacement 赋值为目标节点。
- 最后移除操作一:目标节点不是叶子节点,使用之前赋值到replacement节点替换掉目标节点的位置,并将目标节点移除。
- 若目标节点非红色,则进行红黑树删除平衡调整,因为红色节点删除不影响树的平衡。
- 最后移除操作一:目标节点是叶子节点,直接移除目标节点。
- 将root移到桶索引位的头节点上。
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 检查:若桶不为空且长度不为0,且入参hash值对应索引位不为空
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 若入参的hash值和key都与节点的相同,即为目标节点,赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 3 否则遍历next节点
else if ((e = p.next) != null) {
// 3.1 若目标节点是红黑树,则在树中查找
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 3.2 否则在链表中查找
else {
do {
// 入参的hash值和key都与节点的相同,即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 4 若node不为空证明找到了目标节点,开始操作移除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 若目标节点是红黑树,则用红黑树方式移除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 若目标节点是头节点,则直接将头节点改为目标节点的next节点
else if (node == p)
tab[index] = node.next;
// 4.3 否则将目标节点的next节点,改为其父节点的next节点,完成移除
else
p.next = node.next;
++modCount;
--size;
// 供LinkedHashMap使用
afterNodeRemoval(node);
// 5 返回被移除的节点
return node;
}
}
return null;
}
// 由于红黑树保留了链表结构,因此红黑树移除节点同样也涉及到链表的移除
// 步骤是:
// 1.先处理链表指针
// 2.再处理红黑树结构
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// -------- 处理链表指针 --------
int n;
// 1 检查:桶为空或桶长度为0,直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 2 计算索引位置
int index = (n - 1) & hash;
// 3 将索引位的头节点赋值给first变量和root根
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 4 将目标节点的next节点赋值给succ变量,其prev节点赋值给pred变量
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 5 若pred变量为空,则代表目标节点是头节点,则将其next节点放入头节点
if (pred == null)
tab[index] = first = succ;
// 6 否则将目标节点的前节点的next节点,指向目标节点的next节点
else
pred.next = succ;
// 7 若succ变量不为空,则将succ的prev节点设置为pred(即目标节点next节点的前节点指向目标节点的前节点,相当于目标节点被剔除了)
if (succ != null)
succ.prev = pred;
// 8 若first变量为空,代表该索引位没有节点,直接返回
if (first == null)
return;
// 9 若root的父节点不为空,代表root发生变动了,重置root
if (root.parent != null)
root = root.root();
// 10 默认传过来的movable是true,若根节点为空 或 (根节点子右为空 或 根节点子左为空 或 根节点子左的子左为空),则会将红黑树退回到链表结构
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
// -------- 处理红黑树 --------
// 11 将目标节点赋值给p变量,其子左赋值给pl变量,其子右赋值给pl变量
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 12 检查:若目标节点的子左和子右都不为空时
if (pl != null && pr != null) {
// 12.1 将目标节点的子右赋值给s变量
TreeNode<K,V> s = pr, sl;
// 12.2 从目标节点子右的左支链上一直翻找,直到最左深度赋值给s变量,s要么是目标节点的子右,要么就是其子右的最子左,这里先叫它置换节点
while ((sl = s.left) != null) // find successor
s = sl;
// 12.3 交换目标节点和置换节点的颜色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
// 置换节点的子右赋值给sr变量
TreeNode<K,V> sr = s.right;
// 目标节点的的父节点赋值给pp变量
TreeNode<K,V> pp = p.parent;
// 12.4 第一阶段处理:定位置换节点
// 若置换节点是目标节点的子右,则两节点地理位置互换
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
// 否则
else {
// 置换节点的父节点,赋值给sp变量
TreeNode<K,V> sp = s.parent;
// 置换节点的父节点不为空 则设为 目标节点的父节点
if ((p.parent = sp) != null) {
// 若置换节点是其父节点的子左,则将目标节点 设为 置换节点的父节点的子左
if (s == sp.left)
sp.left = p;
// 将目标节点 设为 置换节点的父节点的子右
else
sp.right = p;
}
// 目标节点的子右 设为 置换节点的子右,若pr不为空,则将置换节点 设为 目标节点子右的父节点
if ((s.right = pr) != null)
pr.parent = s;
}
// 12.5 第二节点处理:置换节点与目标节点位置变动
// 将目标节点的子左 设为 null
p.left = null;
// 将置换节点的子右 设为 目标节点的子右,若sr不为空,则将目标节点 设为 置换节点子右的父节点
if ((p.right = sr) != null)
sr.parent = p;
// 将目标节点的子左 设为 置换节点的子左,若pl不为空,则将置换节点 设为 目标节点子左的父节点
if ((s.left = pl) != null)
pl.parent = s;
// 将目标节点的父节点 设为 置换节点的父节点,若pp为空,则将置换节点设为root根
if ((s.parent = pp) == null)
root = s;
// 若目标节点是其父节点的子左,则将置换节点 设为 目标节点父节点的子左
else if (p == pp.left)
pp.left = s;
// 若目标节点不是root,且是其父节点的子右,则将置换节点 设为 目标节点父节点的子右
else
pp.right = s;
// 12.6 若置换节点的子右不为空,则赋值给replacement变量,所以使用用其子右来 替换 目标节点的位置
if (sr != null)
replacement = sr;
// 12.7 若置换节点的子右为空,则置换节点即为叶子节点,则将目标节点 赋值给replacement,只需将目标节点移除即可
else
replacement = p;
}
// 13 检查:若目标节点的子左不为空,replacement 赋值为其子左
else if (pl != null)
replacement = pl;
// 14 检查:若目标节点的子右不为空,replacement 赋值为其子右
else if (pr != null)
replacement = pr;
// 15 检查:若目标节点的子左子右都为空,replacement 赋值为目标节点
else
replacement = p;
// 第三阶段处理:处理置换后的剩余指针,同时移除目标节点
// 16 检查:若目标节点不是叶子节点,使用replacement节点替换掉目标节点的位置,将目标节点移除
if (replacement != p) {
// 16.1 将目标节点的父节点赋值给replacement节点的父节点
TreeNode<K,V> pp = replacement.parent = p.parent;
// 16.2 若目标节点的父节点为空,则将root节点赋值为replacement节点即可
if (pp == null)
root = replacement;
// 16.3 若目标节点是其父节点的子左,则将目标节点父节点的子左赋值为replacement
else if (p == pp.left)
pp.left = replacement;
// 16.4 若目标节点不为root,且是其父节点的子右,则将目标节点父节点的子右赋值为replacement
else
pp.right = replacement;
// 16.5 目标节点的位置已经被replacement替换,将目标节点所有指向清空方便垃圾回收
p.left = p.right = p.parent = null;
}
// 17 若目标节点非红色,则进行红黑树删除平衡调整,因为红色节点删除不影响树的平衡
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 18 检查:若目标节点为叶子节点,直接移除目标节点
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
// 18.1 将目标节点的父节点 设为 null
p.parent = null;
// 若目标节点的父节点不为空
if (pp != null) {
// 18.2 若目标节点是其父节点的子左,则将父节点的子左指向 null
if (p == pp.left)
pp.left = null;
// 18.3 若目标节点是其父节点的子右,则将父节点的子右指向 null
else if (p == pp.right)
pp.right = null;
}
}
// 19 将root移到桶索引位的头节点上
if (movable)
moveRootToFront(tab, r);
}
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;
}
核心:为什么当目标节点是红黑树时,置换节点的子右是替换目标节点的首选
代码中置换节点是 s 表示,置换节点的子右是 sr 表示,目标节点是 p 表示。
讲这里是以置换节点的子右不为空为前提,置换节点是目标节点的子右向左遍历的终点,此时的 s 已没有子左,而在判断是否用 sr 当作替换目标时,sr 在上部分已经完成了和目标节点的父子相认,成为了目标节点的子右,同时由于目标节点提前将子左置为了空,因此要移除目标节点 p,仅需将 sr 将其覆盖即可,按这种思路设计,sr 即是首选。
核心:红黑树移除节点演示
演示原始场景,目标节点 p 不是根节点 root,且有子左 pl 子右 pr,同时子右 pr 有最深子左 s,最深子左 s 也有子右 sr。
get方法
方法逻辑梳理:
- 检查桶:桶不为空,并且桶的长度大于0,并且要查找的键值对对应的节点不为空。
- 检查节点:节点哈希值和key是否和入参的相同,相同则头节点即为目标节点并直接返回。
- 若头节点不是目标节点,且有next节点则开始遍历链表查找,直接找到目标节点。
- 若是红黑树节点,则开始翻找红黑树:
- 红黑树根节点开始找,红黑树查找为深度优先,并根据入参的hash值大小来判断树枝的方向
- hash值小于遍历到的红黑树节点的hash值,从左子节点继续查
- hash值大于遍历到的红黑树节点的hash值,从右子节点继续查
- 红黑树没有目标节点,返回null
- 链表没有目标节点,返回null。
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 检查桶:桶不为空,并且桶的长度大于0,并且要查找的键值对对应的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2 检查节点:节点哈希值和key是否和入参的相同,相同则头节点即为目标节点并直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3 若头节点不是目标节点,且有next节点则开始遍历链表查找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4 若是红黑树节点,则翻找红黑树
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 5 遍历链表,直至找到目标节点并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6 没有目标节点,返回null
return null;
}
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// h:插入键hash值,k:插入键, kc:插入键class对象
// 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值,向左遍历
if ((ph = p.hash) > h)
p = pl;
// 4 插入的hash值大于p节点的hash值,向右遍历
else if (ph < h)
p = pr;
// 5 插入的key与其hash值等于p节点的key与hash值,直接返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 6 p节点的子左为空,向右遍历
else if (pl == null)
p = pr;
// 7 p节点的子右为空,向左遍历
else if (pr == null)
p = pl;
// 8 若比较的hash值相等,则根据comparable方式比较大小
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 9 若key所属类未实现Comparable, 直接指定向右遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 10 向右遍历为空, 改为向左遍历
else
p = pl;
} while (p != null);
return null;
}