目录
1. 网上关于HashMap的一些问题的错误解析
2. 关于HashMap中一些设计
(1) hash值的计算
(2) 索引的计算
(3)hash表容量必须为2的等次幂
(4) 在链表长度超过8时, 链表可能会转为红黑树
(5) 最小红黑树容量: MIN_TREEIFY_CAPACITY
(6) String类型变量的hash值生成策略
3. HashMap相关定义
成员变量
基本属性默认值
链表节点
红黑树节点
4. HashMap节点的添加: put
相关方法1: putTreeVal
相关方法2: treeifyBin
相关方法3: treeify
相关方法4: moveRootToFront
相关方法5:tableSizeFor
相关方法6: putMapEntries
5. HashMap节点的获取: get
相关方法1:getTreeNode
相关方法2:find
6. HashMap的扩容:resize
相关方法1:split
7.HashMap中节点的删除:remove
相关方法1: removeTreeNode
8.HashMap中红黑树相关方法
左旋转: rotateLeft
右旋转: rotateRight
插入后调整红黑树平衡:balanceInsertion
删除后调整红黑树平衡:balanceDeletion
为什么负载因子默认为 0.75?
关于负载因子的设定, 网上大部分都是粘贴出来源码中的注释:
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
大概翻译如下:
因为TreeNode的大小约为链表节点的两倍,所以仅在一个链表中包含足够多的节点时才会转为红黑树(参考TREEIFY_THRESHOLD)。 当它们变得太小时(由于移除或调整大小), 我们会将红黑树转为链表, 如果一个用户的数据他的hashcode值分布十分好的情况下,将会很少使用到红黑树结构。在理想情况下,使用随机hashCode值下, 负载因子为0.75,尽管由于粒度调整会产生较大的方差, key经过hash计算后进入某一个桶的概率为0.5, 即桶中的Node的分布频率服从参数为0.5的泊松分布, 预期列表大小k的出现是(exp(-0.5) * pow(0.5, k) / * factorial(k))
因为这个是在jdk1.8中HashMap上的注释, 并且也提到了负载因子为0.75, 很多人并没有好好理解这段话的意思, 就强行将泊松分布于负载因子关联在一起。但是这段话的意思在于说明为什么TREEIFY_THRESHOLD的值会设置为8, 也就是为什么HashMap中链表长度超过8时才会转为红黑树, HashMap中TREEIFY_THRESHOLD的设定是为了尽可能的避免出现链表转红黑树的情况, 但是另一方面需要防止在恶意的数据规模中, 出现HashMap退化为链表的情况; 在注释中提到了负载因子, 这里的意思是, 注释中泊松分布的数据模型是基于负载因为为0.75的情况进行推演的;
对于为什么会默认为0.75, 目前还没有找到最严谨的推算方式, 也有说可以利用Hash碰撞通过二项式进行推算, 但是Hash碰撞并不完全符合二项分布的条件...
不论是链表节点还是红黑树节点, 其中都维护着该节点key的hash值, 因为在hashMap的各个方法中都频繁的应用到了hash值; 在红黑树中计算索引时增加了扰动计算, 也就是在计算key的hash值后, 将key的的高16位与低16位进行异或运算, 减少hash碰撞
在计算索引时并没有使用%进行取模运算, 而是使用了hash值与hash表长度-1进行与运算来获取hash值对应的索引, 提高计算效率
hash: 1 0 1 0
&
tabCap-1: 1 1 1
=
index: 0 1 0
hash表容量为1000, 那么在hash值中低于第四位的值都是hashCap的模
根据上面索引的计算可以得出, 之后在hash表长度为2的等次幂, 在转为二进制数值后, 除了最高位其他位的数值均是0, 在tabCap-1, 所有位置的数值均为1, 与hash值进行与运算获取到模, 即索引
HashMap每个桶中可能存在值的几率是0.5, 即λ = 0.5
泊松分布公式: exp(-0.5) * pow(0.5, k) / factorial(k)
多个节点经过hash计算索引后, 在出现hash冲突, 在同一个桶内的概率符合泊松分布
(注意:下面的数据模型是基于负载因子为0.75):
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
当8个节点出现hash冲突在同一个桶内的概率为0.00000006, 所以在链表长度超过8的时候【可能】转为红黑树,
可见Hash表这样设计的本意是为了避免出现链表转为红黑树的情况
TREEIFY_THRESHOLD的设定是为了避免在出现链表转红黑树的情况, 但是另一方面需要防止在特定的数
据规模中,出现HashMap退化为链表导致查询效率急剧下降的情况
HashMap中链表长度超过8的时候【可能】转为红黑树;
当链表长度超过8时, 首先判断hash表的长度是否大于最小红黑树容量(也就是hash表中出现红黑树需要达到的最小容量);
大于等于最小红黑树容量才会将链表转换为红黑树, 【如果小于, 则进行的是扩容操作, 而不是转换红黑树】
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
示例:
字符串 java,由 j、a、v、a 四个字符组成
因此,java 的哈希值可以表示为 j ∗ n^3 + a ∗ n^2 + v ∗ n^1 + a ∗ n^0, 等价于 [ ( j ∗ n + a ) ∗ n + v ] ∗ n + a
在JDK中,乘数 n 为 31,为什么使用 31?
(1) 31符合2^n – 1,JVM会将 31 * i 优化成 (i << 5) – i
(2) 31是一个奇素数(既是奇数,又是素数,也就是质数), 素数和其他数相乘的结果比其他方式更容易产成唯一性,减少哈希冲突
还有其他在后面的源码解析中体现, 会有相关注明
/**
* 桶; 存储链表的头节点的表, 或红黑树的根节点的表
*/
transient Node[] table;
/**
* 存储缓存的set集合
*/
transient Set> entrySet;
/**
* HashMap中实际存储数据的数量
*/
transient int size;
/**
* HashMap结构上被修改的次数
*/
transient int modCount;
/**
* 阈值, 当size超过该阈值, 则进行扩容; ( 容量 * 负载系数 ) 16 * 0.75f = 12,
*/
int threshold;
/**
* 负载因子 0.75
* 负载因子的用处, 最大可能的避免出现Hash冲突
*/
final float loadFactor;
/**
* 默认初始化容量 - 必须为2的等次幂 (编译后面利用左移右移进行计算)
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量, 必须小于2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子为0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
*
* 当链表长度达到8时, 将链表转为红黑树;
* 注意: HashMap每个slot可能存在值的几率是0.5, 即λ = 0.5
* 泊松分布公式: exp(-0.5) * pow(0.5, k) / factorial(k)
* 多个节点经过hash计算索引后, 在出现hash冲突, 在同一个桶内的概率符合泊松分布
* (注意:下面的数据模型是基于负载因子为0.75):
*
* 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
*
* 当8个节点出现hash冲突在同一个桶内的概率为0.00000006, 所以在链表长度超过8的时候【可能】转为红黑树,
* 可见Hash表这样设计的本意是为了避免出现链表转为红黑树的情况
*
* TREEIFY_THRESHOLD的设定是为了避免在出现链表转红黑树的情况, 但是另一方面需要防止在特定的数
* 据规模中,出现HashMap退化为链表导致查询效率急剧下降的情况
*
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当红黑树中的节点数量小于6时,红黑树将转换为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*
* HashMap中链表长度超过8的时候【可能】转为红黑树
* 当链表长度超过8时, 首先判断hash表的长度是否大于最小红黑树容量(也就是hash表中出现红黑树需要达到的最小容量)
* 大于等于最小红黑树容量才会将链表转换为红黑树, 【如果小于, 则进行的是扩容操作, 而不是转换红黑树】
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Node节点,
* 1.存储节点数据(key, value)
* 2.存储key的hash值
* 3.下一个节点的索引(用于链表模式下指向下一个节点, 单向链表)
* 为什么使用单向链表?
* 1. 节省内存, 仅存储下一个节点的地址即可
* 2. 当出现hash值相同的key时, key会遍历该链表所有的节点, 判断是否存在相同的key,
* 如果有, 则覆盖该节点. 如果查询至链表尾部, 仍然没有相同key,则就在该链表尾部新增节点
*/
static class Node implements Map.Entry {
final int hash; //存储key的hash值, 用于计算该key在Hash表中的索引位置
final K key; //key值, 不允许修改, 覆盖 (注:如果出现相同key,则直接覆盖value即可)
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() {
//key的hash值与value的hash值的异或运算
//异或运算: 0^0=0,0^1=1,1^0=1,1^1=0; 即: 参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue; //在更新value后, 会返回旧的value值
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
//key与value都需要根据各自定义的equals方法认定为相同
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
注意: 红黑树节点继承链表节点, 因此在红黑树中是维护着链表的前后顺序的
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // 父节点
TreeNode left; //左子节点
TreeNode right; //右子节点
TreeNode prev; // 维持红黑树中节点的添加顺序 (注意: 在删除节点后, 需要取消连接)
boolean red; // 红黑树节点颜色, 默认为红色
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
/**
* 返回节点的根节点
*/
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
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; //n: hash表的长度; i: 目标key在hash表中的索引
if ((tab = table) == null || (n = tab.length) == 0)
/**
* 如果当前hash表为空, 或者长度为0 则初始化 【懒加载】
*/
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
/**
* 如果对应hash桶的位置没有节点, 那么直接将当前节点放到此位置中
* p为当前节点key的hash的索引值所对应的桶节点, 在判断语句中已经进行赋值操作
*/
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
/**
* 如果当前桶节点的hash值与新增节点的hash值相同, 并且key也相同, 即覆盖当前节点的value即可
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 【程序执行到这里】: 当前桶节点的key与新增节点的key不相同
* 红黑树节点处理
*/
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); //如果在红黑树中存在相同key的节点, 那么这里会返回该节点, 便于后面统一进行覆盖操作
/**
* 链表节点处理
*/
else {
for (int binCount = 0; ; ++binCount) { //计算当前链表的长度, 后面判断如果大于8, 需要转为红黑树
/**
* 如果查找至链表尾部, 还未找到相同key, 那么直接添加至尾部即可;
* 这个也就是为什么新增的节点需要插入到链表尾部, 反正都要到达链表尾部的
*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
/**
* 这里为什么要对TREEIFY_THRESHOLD-1?
* 因为在本次插入节点后, 链表的长度已经增长了1
*/
if (binCount >= TREEIFY_THRESHOLD - 1)
/**
* 符合调条件, 当前链表可能需要转为红黑树
*/
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
/**
* 【当程序执行到这里】: 待插入节点与链表中的某个节点的key一致! 因为在并发场景中, 可能在插入新节点之前其他线程已经插入了一个与当前节点相同key的节点,
* 为了保证hashmap中key的唯一性, 所以需要做最后的校验
*/
break;
p = e;
} //for
}
/**
* 【当程序执行到这里】: 如果e不为null, 那么说明在hashmap中存在于待插入的节点相同的key, 那么只需要覆盖该节点的value值即可
*/
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
/**
* 如果允许覆盖旧的value 或者 旧的value为bull 才会进行覆盖
*/
e.value = value;
afterNodeAccess(e); //linkedHashMap实现
return oldValue; //返回覆盖的旧value值
}
}
++modCount;
/**
* 在添加新的元素后, 判断是否超过阈值, 如果超过, 则进行扩容
*/
if (++size > threshold)
resize();
afterNodeInsertion(evict);//linkedHashMap实现
return null;
}
/**
* 向红黑树中添加元素
*/
final TreeNode putTreeVal(HashMap map, Node[] tab, int h, K k, V v) {
//map: 当前hashmap实例, tab: hash表, h: key的hash值, k: key值, v:value值
Class> kc = null;
boolean searched = false; //是否进行了全局扫描搜索 (全局扫描的目的:是否已经存在即将插入的节点key)
TreeNode root = (parent != null) ? root() : this; //确保传入当前节点为根节点
/**
* 从根节点开始遍历, 找到正确的插入位置
*/
for (TreeNode p = root;;) {
int dir, ph; K pk; //dir: 新增节点与当前节点大小比较的差值, ph: 当前节点的hash值, pk: 当前节点的key, p:当前节点(待比较节点)
/**
* 新增节点key的hash值 小于 当前节点key的hash值, 说明新增节点需要在当前的左树中插入
*/
if ((ph = p.hash) > h)
dir = -1;
/**
* 新增节点key的hash值 大于 当前节点key的hash值, 说明新增节点需要在当前的右树中插入
*/
else if (ph < h)
dir = 1;
/**
* 如果新增节点key的hash值与当前节点的hash值相等, 那么需要比较key是否相同(比较两个key的地址以及 使用equals方法判定); 如果相同, 则返回该节点
* 【注意】:此处没有覆盖操作, 这里仅仅返回重复节点, 在putVal方法中后面会统一进行覆盖value操作
*/
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
* 【程序执行到这里】: 新增节点key的hash值与当前节点的hash值相等, 但是key并不相等
*
* 需要判断两个节点是否具有可比较性
* 如果具有可比较性, 那么进行比较, 判断大小, 来决定接下来新增节点是在当前节点的左子树还是右子树中插入;
*/
else if (
(kc == null && (kc = comparableClassFor(k)) == null) //两个节点不具备可比较性
||
(dir = compareComparables(kc, k, pk)) == 0) { //两个节点具备可比较性, 但是通过比较方法判定两个节点的key大小相等
/**
* 对当前节点的左子树和右子树进行全局扫描, 判断新增节点key是否已经存在
* 1.两个节点不具备可比较性
* 2.两个节点具备可比较性, 但是通过比较方法判定两个节点的key大小相等
*
* 【注意】:此处比较大小, 只能比较两个key的大小, 不能判定两个key相同, 两个key相同只能使用equals()方法判定 或者 内存地址一致
*/
if (!searched) {
TreeNode q, ch;
searched = true; // 全局扫描比较消耗性能, 所以只进行一次即可
if ( ( (ch = p.left) != null && (q = ch.find(h, k, kc)) != null ) //在当前节点的左子树中查找是否存在新增节点的key
||
( (ch = p.right) != null && (q = ch.find(h, k, kc)) != null ) ) //在当前节点的右子树中查找是否存在新增节点的key
return q;
}
/**
* 【程序执行到这里】: 此时新增节点已经明确不存在于当前红黑树中, 所以需要判断接下来需要插入到当前节点的左子树还是右子树中
* 由于新增节点和当前节点不具有可比较性, 那么只能通过两个key的内存地址的hash值大小来比较
*/
dir = tieBreakOrder(k, pk);
}
/**
* 【程序执行到这里】: 已经确认新增节点需要向当前节点左子树中插入还是右子树中插入, 那么接下来开始准备下一轮的判断
*/
TreeNode xp = p; //xp: 当前节点
/**
* 如果已定位至当前节点 ,那么需要根据新增节点与当前节点大小的差值, 判断新增节点为该叶子节点的左子节点还是右子节点
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next; //xpn: 当前节点的下一个节点
TreeNode x = map.newTreeNode(h, k, v, xpn); //根据key,value创建新节点, 准备插入, x: 新增节点
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;
}
} //for循环结束
}
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e; //n:hash表的长度, index:新增节点在hash表中索引位置, e:桶节点
/**
* 如果hash表为null 或者 hash表的长度大于等于最小红黑树容量 (也就是hash表中出现红黑树需要达到的最小容量)才会将链表转换为红黑树
*/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//如果小于, 则进行的是【扩容操作】, 而不是转换红黑树
resize();
/**
* 符合转红黑树条件
* 【1】:链表长度超过8
* 【2】:hash表的长度大于最小红黑树容量64
*
* (n - 1) & hash ==> 取模,获取索引
*/
else if ((e = tab[index = (n - 1) & hash]) != null) {
/**
* 如果当前桶节点不为空
* 前面已经判断过该桶节点不为空了, 此处为什么还要再次判断 ?
* 因为, hashmap中有可能是存在并发操作....
*/
TreeNode hd = null, tl = null; //td: 链表的第一个节点
do {
/**
* 将链表中Node节点转为TreeNode节点
* 由于之前是链表, 所以该链表的所有节点均为Node, 但是, 接下来需要转为红黑树了, 就不能再使用Node节点, 而是TreeNode节点
*/
TreeNode p = replacementTreeNode(e, null);
if (tl == null)
hd = p; //获取到链表节点转红黑树节点后的第一个节点, 后面需要根据这个节点将链表转为红黑树
else {
p.prev = tl; //维护红黑树节点中的prev属性
tl.next = p; //维护链表中的next属性
}
tl = p;
} while ((e = e.next) != null);
/**
* 链表转红黑树
*/
if ((tab[index] = hd) != null)
hd.treeify(tab); //this为链表的第一个节点
}
}
/**
* Forms tree of the nodes linked from this node.
* @return root of tree
*/
final void treeify(Node[] tab) {
TreeNode root = null; //初始化红黑树的根节点为null
/**
* 从链表的第一个节点开始遍历, 转换
*/
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next; //next: 当前节点的下一个节点
x.left = x.right = null; //初始化当前节点左右子节点都为null
/**
* 如果root节点未确认, 那么将当前节点作为根节点, 并置为黑色(红黑树性质:根节点为黑色)
*/
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
/**
* 【当程序执行到这里】: root节点已经确认, 第一次执行到else中时, x节点为此时root节点在链表中的下一个节点
* 生成红黑树
*/
else {
K k = x.key; //当前节点的key
int h = x.hash; //当前节点key的hash值
Class> kc = null; //可比较节点的类对象
/**
* 每次都需要从根节点开始找到合适的位置插入当前节点
* 在查找过程中不需要判断当前节点是否与红黑树中的节点通过equals判定为相同
*/
for (TreeNode p = root;;) {
int dir, ph; //dir: 当前节点与根节点大小差值, ph: 根节点节点的hash值
K pk = p.key; //根节点节点的key
/**
* 判断当前节点需要插入到根节点的左子树还是右子树中
*/
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
/**
* 如果当前节点与根节点key的hash值相同, 则判断是否具有可比较性, 根据compareTo方法判断大小
*/
else if ( (kc == null && (kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0 )
/**
* 如果当前节点与根节点不具备可比较性, 或者具备可比较性,但是被compareTo方法判定为大小相同
* 则比较两个节点key内存地址的hash值大小
*/
dir = tieBreakOrder(k, pk);
/**
* 【当程序执行到这里】: 已经判断了根节点与当前节点的大小, 接下来根据差值, 判断是向根节点的左子树中插入还是右子树中插入
*/
TreeNode xp = p;
/**
* 根据差值判断是向根节点的左子树是右子树中查找, 并且是否到达最底层, 无法再向下查找; 如果没有到达最底层, 那么需要在该树中继续向下查找
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
/**
* 找到合适的节点位置, 开始插入
* 注意在转为红黑树后, 节点中仍然维护着链表中原有的prev属性
*/
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
/**
* 在插入后,修复红黑树性质
*/
root = balanceInsertion(root, x);
break;
}
}
} //else
} //for
/**
* 将红黑树的root节点移动到hash表相对应的索引位置
*/
moveRootToFront(tab, root);
}
/**
* Ensures that the given root is the first node of its bin.
* 【1】 将红黑树的root节点移动到hash表相对应的索引位置
* 【2】 将红黑树的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;
// 原链表的头节点
TreeNode first = (TreeNode)tab[index];
/**
* 如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
*/
if (root != first) {
Node rn; //rn:后一个节点
//将该索引位置的头节点赋值为root节点
tab[index] = root;
TreeNode rp = root.prev; //root节点前一个节点
/**
* 删除root节点在原有链表中前后引用关系, 将root节点从链表中"删除", 因为后面要将root节点添加到链表的头部
*/
// 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;
// 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
if (rp != null)
rp.next = rn;
/**
* 将原有链表的第一个节点设为root节点的下一个节点
*/
if (first != null)
// 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
first.prev = root;
// root的next指向头节点
root.next = first;
// root的prev指向null
root.prev = null;
}
/**
* 这一步是防御性的编程
* 校验TreeNode对象是否满足红黑树和双链表的特性
* 如果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发场景下);也可能是TreeNode的实现有问题(这个是理论上的以防万一);
**/
assert checkInvariants(root);
}
}
/**
* Returns a power of two size for the given target capacity.
* 找到【大于等于】给定容量的最小2的次幂值
* 例如: cap = 15, return 16;
* HashMap 中没有 capacity 属性,
* 初始化时,如果传了初始化容量值,该值是存在 threshold 变量,并且hash数组是在第一次 put 时才会进行初始化,
* 初始化时会将此时的 threshold 值作为新表的 capacity 值,然后用 capacity 和 loadFactor 计算新表的真正 threshold 值
* 而此方法就是根据threshold(临时作为capacity)计算出真正的capacity
*/
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;
}
/**
* Implements Map.putAll and Map constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
* 将给定map的所有元素放入当前的map中, 给定的map必须实现Map.putAll方法和Map的构造函数
*/
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size(); //给定map中的元素数量
if (s > 0) {
if (table == null) { // pre-size
/**
* 此处根据给定map中实际元素的数量, 推算容纳下给定map中所有元素所需要的最小容量
* size <= threshold = capacity * 0.75
* 如果想要capacity取得最小值, 那么threshold等于size即可, 因此可以通过可以通过size直接推算最小容量
*/
float ft = ((float)s / loadFactor) + 1.0F; //计算新的hash表能够容纳下给定map中所有元素所需要的最小capacity
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
if (t > threshold) //如果最小容量大于当前hash的阈值, 则更新阈值为大于等于当前容量的最小2的等次幂 例:s = 3, t = 5, threshold = 8
/**
* 此处的threshold为新hash表的最小容量, 后面threshold将赋给newCap, 而threshold将根据newCap * 0.75重新计算
* 此处为什么要使用threshold来表示最小容量?
* HashMap 有 threshold 属性和 loadFactor 属性,但是没有 capacity 属性。
* 初始化时,如果传了初始化容量值,该值是存在 threshold 变量,
* 并且 Node 数组是在第一次 put 时才会进行初始化,初始化时会将此时的 threshold 值作为新表的 capacity 值,
* 然后用 capacity 和 loadFactor 计算新表的真正 threshold 值。
*
* Hash表的容量必须为2的等次幂, 所以此处需要计算大于等于期望容量的最小值
*/
threshold = tableSizeFor(t);
}
else if (s > threshold)
/**
* 如果当前hash表不为null,且单单给定map中的元素就超过了阈值, 则提前扩容, 在遍历插入, 在插入的过程中也有可能进行扩容
* 其他情况则在put的时候进行扩容
*/
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) { //遍历给定map,向当前map中put元素
K key = e.getKey();
V value = e.getValue();
/**
* 向目标hash表中添加元素
*/
putVal(hash(key), key, value, false, evict);
}
}
}
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 根据key的hash值获取value
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
/**
* hash表不为空且长度大于0
* 获取到根据hash计算索引,获取到第一个节点(该节点可能为链表节点或者红黑树根节点)
*/
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
/**
* 判断key是否相同的条件: 两个key的内存地址相同 或者 两个key通过equals方法判定为相同
* 判断第一个节点是否是目标节点(注意:判断两个节点相同只能使用equals方法, 而compareTo()仅用于比较两个可比较节点的大小, 不能作为是否相同
*/
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))) )
return first;
//如果头节点的下一个节点不为null才会去查找, 否则直接返回null
if ((e = first.next) != null) {
/**
* 1.如果头节点为红黑树节点, 需要到红黑树找
* 调用红黑树的方法获取并返回指定key的节点
*/
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
/**
* 2. 如果头节点为链表, 循环遍历链表寻找目标节点
*/
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
/**
* Calls find for root node.
*/
final TreeNode getTreeNode(int h, Object k) {
/**
* 找到当前节点的根节点, 然后开始查找指定节点
*/
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 返回节点的根节点
*/
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*
* 在红黑树中查找指定节点
*
* 注意: 在find方法中比较的是hash值大小, 在某棵红黑树中, 所有的节点在hash表中的索引是一样的, 但是hash值不一定相同
*/
final TreeNode find(int h, Object k, Class> kc) {
TreeNode p = this; //this: 当前节点, h:目标key的hash值, k:目标节点的key, kc:
do {
int ph, dir; //ph: 当前节点的hash值; dir:
K pk; //当前节点的key
TreeNode pl = p.left, pr = p.right, q; //pl当前节点的左子节点, pr当前节点的右子节点
/**
* 获取当前节点的hash值, 比较hash值
* 如果目标key的hash小于当前节点key的hash值, 则向该红黑树的左子树查找
*/
if ((ph = p.hash) > h)
p = pl;
/**
* 如果目标key的hash大于当前节点key的hash值, 则向该红黑树的右子树查找
*/
else if (ph < h)
p = pr;
/**
* 【程序到达这里】: 当前节点与目标节点hash值相同, 开始判定key是否相同:
* key相同需要满足的条件:
* 1.目标key的hash等于当前节点key的hash值
* 2.当前节点的key与目标节点的key内存地址相同 或者 当前节点的key与目标节点的key通过equals判定为相同
*/
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
* 【程序到达这里】: 当前节点与目标节点hash值相同, 但是key不相同, 也就是目标节点可能在当前的节点子树中;
* 思考: 此时当前节点的hash值与目标节点的hash值相同, 但是又不是同一个节点, 那么
* 接下来需要接着向下查找, 但是由于hash值相同, 将无法判断目标节点在当前的节点的
* 左子树还是右子树中, 所以如果节点存在可比较性, 将根据compareTo方法进行比较;如
* 若节点不存在可比较性, 那么将采用层序遍历, 向下逐个查找;
*
* 当前节点的左子节点为null,即不存在左子树, 那么将准备在当前节点的右子树中查找目标key
*/
else if (pl == null)
p = pr;
/**
* 当前节点的右子节点为null,即不存在右子树, 那么将准备在当前节点的左子树中查找目标key
*/
else if (pr == null)
p = pl;
/**
* 【程序到达这里】: 当前节点的左右子树都不为空, 那么需要判断节点是否具有可比较性
*/
else if ( (kc != null || (kc = comparableClassFor(k)) != null) //判断key是否具有可比较性(是否实现Comparable接口)
&&
(dir = compareComparables(kc, k, pk)) != 0 ) //dir: 目标节点key与当前节点key比较的差值
/**
* 如果dir小于0, 说明目标节点key小于当前节点key,那么将开始向当前节点的左子树中查找
* 如果dir大于0, 说明目标节点key大于当前节点key,那么将开始向当前节点的右子树中查找
*/
p = (dir < 0) ? pl : pr;
/**
* 如果dir等于0, 说明目标节点key的大小等于当前节点key的大小, 但是不能判定为两个key相同,
* 两个key相同只能通过内存地址,或者equals方法判断; 如果dir==0, 那么将开始分别向左右子树中查找;
*
* 默认向开始递归向当前节点的右子树中查找, 如果找到则直接返回, 如果没有找到,则开始向左子树中查找
*/
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
/**
* 旧hash表进行扩容
*/
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
/**
* 【程序执行到这里】: 目标hash表的旧容量为空, 但是旧阈值不为空, 也就说明此时是需要进行初始化的
* 而旧的阈值oldThr 就是 提前计算好需要的hash表的初始化容量
*/
newCap = oldThr;
else {
/**
* 【程序执行到这里】: 目标hash表的旧容量和旧阈值均为空, 需要将容量和阈值设置为默认值
*/
newCap = DEFAULT_INITIAL_CAPACITY; //默认容量:16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //默认阈值:12
}
if (newThr == 0) {
/**
* 【程序执行到这里】: 新的容量已经计算出来, 但是新的阈值还未进行计算
*/
float ft = (float)newCap * loadFactor; //新的容量 * 负载因子 = 新的阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //更新阈值
/**
* 根据新的容量创建hash表
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
/**
* 如果就得hash表不为空, 那么需要进行元素挪动, 即需要把旧的hash表上的元素挪到新的hash表上
*/
if (oldTab != null) {
/**
* 遍历旧的hash表的所有桶节点, 开始挪动元素
*/
for (int j = 0; j < oldCap; ++j) {
Node e; // e: 准备挪动的节点
//如果该桶节点不为空,则开始挪动
if ((e = oldTab[j]) != null) {
oldTab[j] = null; //清除原桶节点的引用
if (e.next == null)
/**
* 如果桶节点的下一个节点为空, 那么说明该hash桶中只有一个节点, 直接计算新的索引, 然后挪动就可以了
* 注意:如果在旧的hash表中某个桶的位置只有一个元素, 那么在新的hash表中, 该元素所对应的新的hash桶位置也将只有一个元素
*/
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/**
* 如果当前节点是红黑树节点, 则强转为TreeNode节点, 利用红黑树性质挪动节点
*/
((TreeNode)e).split(this, newTab, j, oldCap);
else { // 保持顺序
/**
* 如果当前节点是链表节点, 那么会根据索引位置将该链表拆为两个链表
*/
Node loHead = null, loTail = null; //链表1
Node hiHead = null, hiTail = null; //链表2
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);
/**
* 将lo链表放到对应的hash桶中
*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
/**
* 将hi链表放到对应的hash桶中
*/
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* 在扩容后, 红黑树中的节点,只会存在于两个位置: 原索引的位置, 原索引+旧容量
* 为什么?
* 假设:
* (1)某个节点key的hash值为 10(1010), hashMap旧容量为 4(100), 新容量为 8 (1000)
* hash: 1010
* oldCap-1: 11
* oldIndex: 10
*
* hash: 1010
* newCap-1: 111
* newIndex: 010
*
* oldIndex == newIndex
*
* (2)某个节点key的hash值为 14(1110), hashMap旧容量为 4(100), 新容量为 8 (1000)
* hash: 1110
* oldCap-1: 11
* oldIndex: 10
*
* hash: 1110
* newCap-1: 111
* newIndex: 110
*
* oldIndex == newIndex + oldCap
*
* 如何判断索引是在原位置, 还是在原位置+oldCap呢?
* 在上面的两个例子中, 可以看出, 新的索引在哪个位置取决于hash表中oldCap容量的最高位对应hash值的那一位是 0 还是 1
* 也就是可以通过 hash与oldCap进行&运算, 可以计算出该位置是 0 还是 1, 如果是0, 则在原位置, 如果是1,则在原位置+oldCap
*
*/
final void split(HashMap map, Node[] tab, int index, int bit) {
//map: 当前hashmap对象, tab: 新的hash表, index:当前节点所在hash表中位置, bit:旧容量
TreeNode b = this;
// Relink into lo and hi lists, 保持节点顺序
TreeNode loHead = null, loTail = null; //存储索引位置为"原索引位置"的节点 (注意链表的节点为TreeNode)
TreeNode hiHead = null, hiTail = null; //存储索引位置为"原索引位置+旧容量"的节点 (注意链表的节点为TreeNode)
int lc = 0, hc = 0;
for (TreeNode e = b, next; e != null; e = next) { //遍历整个红黑树节点, 移动节点到新位置上
next = (TreeNode)e.next; // next为当前节点的下一个节点
e.next = null; //先将当前节点的next属性置空
/**
* 节点索引在扩容后前后一致
*/
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) //如果loTail为空, 说明是第一个节点
loHead = e; //给头节点赋值
else
loTail.next = e; //向链表尾部添加节点
loTail = e; //更新链表尾部节点
++lc; //统计原索引位置节点的数量
}
/**
* 节点索引在扩容后, 新的位置为原位置+旧容量
*/
else {
if ((e.prev = hiTail) == null) //如果hiTail为空, 说明是第一个节点
hiHead = e; //给头节点赋值
else
hiTail.next = e; //向链表尾部添加节点
hiTail = e; //更新链表尾部节点
++hc; //统计新索引位置节点的数量
}
}//for
/**
* 【当程序执行到这里】: 原红黑树已经拆分为两个链表, 如果链表不为null, 那么该链表将更新到hash桶对应位置上
* 接下来需要根据每个链表的长度, 判断时候需要将红黑树装化为链表;
*/
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
/**
* 如果链表长度小于等于UNTREEIFY_THRESHOLD, 那么不需要转为红黑树, 保持为链表即可
* 那为什么这里调用了红黑树的untreeify方法, 因为在上面维护两个链表的节点是TreeNode, 这里需要转为链表节点Node, 节省内存使用
*/
tab[index] = loHead.untreeify(map);
else {
/**
* 该链表需要转为红黑树, 那么先将链表赋给对应的hash桶中
*/
tab[index] = loHead;
if (hiHead != null)
/**
* 如果hiHead不为null, 才会转为红黑树
* 因为, 如果hi链表为null, 说明在扩容前后, 红黑树上节点的位置在红黑树中没有发生任何改变, 并且该红黑树仍然在原hash桶位置
* 如果hi链表不为null, 说明hi链表分走了一部分节点, 但是hi链表仍然没有到达转为链表的条件, 所以需要转为红黑树
*/
loHead.treeify(tab);
}
}
/**
* 处理hi链表
*/
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
/**
* 如果链表长度小于等于UNTREEIFY_THRESHOLD, 那么不需要转为红黑树, 保持为链表即可
* 那为什么这里调用了红黑树的untreeify方法, 因为在上面维护两个链表的节点是TreeNode, 这里需要转为链表节点Node, 节省内存使用
*
* 更新链表位置在索引+旧容量
*/
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
/**
* 如果loHead不为null, 才会转为红黑树
* 因为: 如果lo链表为null, 说明在扩容前后, 红黑树上节点的位置在红黑树中没有发生任何改变, 仅需要修改根节点在hash桶中的位置,
* 如果lo链表不为null, 说明lo链表分走了一部分节点, 但是hi链表仍然没有到达转为链表的条件, 所以需要转为红黑树
*/
hiHead.treeify(tab);
}
}
}
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
//tab:hash表, p:key所在hash桶的第一个节点(链表的首节点或者红黑树的根节点), n: 表的长度, index:索引
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))))
/**
* 如果hash桶中的第一个节点就是待删除节点
*/
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; //p为前一个节点, e为即将要查找的节点
} while ((e = e.next) != null);
}
}
/**
* 【当程序执行到这里】: node为待删除的节点, 可能为红黑树节点, 也可能为链表节点
* 删除操作的前置条件:
* (1)待删除节点不为空
* (2)需要匹配value相同 并且 查找到的value和给定的value一致(内存地址一致或者equals判定为一致)
* 或者
* 不匹配value, 只要key相同就删除
*/
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)
/**
* 如果hash桶中的第一个节点就是待删除节点
*/
tab[index] = node.next;
else
/**
* 如果是链表节点
* p为待删除节点的前一个节点, node为待删除节点
* 删除单向链表节点, 只修改next引用即可, 待删除节点的前一个节点的next属性指向待删除节点的下一个节点
*/
p.next = node.next;
++modCount;
--size; //size减一
afterNodeRemoval(node); //linkedHashMap实现
return node;
}
}
return null;
}
final void removeTreeNode(HashMap map, Node[] tab, boolean movable) {
/**
* this: 待删除节点, movable:如果为false, 则在移除结构时不要移动其他节点
*/
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash; //index:计算待删除节点在hash表中的索引, hash:为待删除节点的hash值
TreeNode first = (TreeNode)tab[index], root = first, rl; //root:红黑树的根节点, rl:红黑树的左子节点
TreeNode succ = (TreeNode)next, pred = prev; //succ:链表属性的中下一个节点, pred:红黑树属性中的上一个节点
if (pred == null) //如果上一个节点为空,说明删除的节点为红黑树的根节点
tab[index] = first = succ;
else
pred.next = succ; //如果不是根节点, 那么将待删除节点从链表引用中"移除", 待删除节点的前一个节点的next指向删除节点的下一个节点
if (succ != null)
succ.prev = pred; //待删除节点的后一个节点的prev指向删除节点的上一个节点
if (first == null) //如果此时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); // 通过根节点判断当前红黑树是否太小了, 如果是,则转为链表
return;
}
/**
* 上面链表的引用已维护完成, 接下来开始维护红黑树的相关引用
*/
TreeNode p = this, pl = left, pr = right, replacement; //p:待删除的节点, pl:左子节点, pr:右子节点
/**
* 待删除节点的度为 2, 即:红黑树的左右子节点都不为null, 需要找后继节点, 然后让后继节点替换待删除节点, 最终删除的还是度为0或者1的后继节点
* 因此: 可得出结论, 在删除节点时,最终被删除的都是度为1或者0的节点
* 如何找到后继节点? 当前节点右子节点的左子树最小的节点
*/
if (pl != null && pr != null) {
TreeNode s = pr, sl; //pr:待删除节点的右子节点, sl:待删除节点的右子节点的左子节点, s:待删除节点的后继节点, 也就是真正被删除的节点,
/**
* 1. 循环找到后继节点
*/
while ((sl = s.left) != null)
s = sl;
/**
* 交换待删除节点与真正被删除节点(待删除节点的后继节点) 的颜色
*/
boolean c = s.red; s.red = p.red; p.red = c;
/**
* 2. 找到后继节点被删除后的替代节点
* (当后继节点的度为1时, 在后继节点被删除后, 需要让后继节点的右子节点替代后继节点)
*/
TreeNode sr = s.right; // 后继节点的右子节点 (后继节点要么只有一个右子节点, 要么没有子节点)
TreeNode pp = p.parent; //待删除节点的父节点
/**
* 如果后继节点为待删除节点的右子节点, 那么直接让后继节点替换父节点引用关系即可
*/
if (s == pr) {
p.parent = s;
s.right = p;
}
else { //如果后继节点为待删除节点的右子树中的某个节点的左子节点, 替换后继节点与待删除节点的引用关系
TreeNode sp = s.parent; //后继节点的父节点
/**
* 【将待删除节点"移动"到后继节点的位置, 让后继节点"移动"到待删除节点的位置】, 维护后继节点的父节点与待删除节点的引用关系
*/
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p; //如果后继节点是父节点的左子树
else
sp.right = p;//如果后继节点是父节点的右子树 (疑问? 后继节点不可能为待删除节点右子树中的右子节点吧...)
}
/**
* 维护后继节点的右子节点引用
* 将待删除节点的右子节点作为后继节点的右子节点, 并且将待删除节点的右子节点的parent属性引用改为后继节点
*/
if ((s.right = pr) != null)
pr.parent = s;
}
/**
* 维护后继节点与待删除节点的父节点和子节点的引用关系
*/
p.left = null; // 替换后的p的左子节点为null
if ((p.right = sr) != null) //如果后继节点的右子节点不为空, 维护右子节点与待删除节点引用关系
sr.parent = p;
if ((s.left = pl) != null) //维护待删除节点左子树的引用关系
pl.parent = s;
if ((s.parent = pp) == null) //将后继节点的parent属性指向待删除节点的父节点
root = s;
else if (p == pp.left)
pp.left = s; //如果待删除节点为左子节点, 那么需要将待删除节点的父节点的left属性指向后继节点
else
pp.right = s; //如果待删除节点为右子节点, 那么需要将待删除节点的父节点的right属性指向后继节点
/**
* 【当程序执行到这里】:待删除节点"移动"到后继节点的位置, 后继节点"移动"到待删除节点的位置
* replacement: 在后继节点被删除后, 替代后继节点的节点
*/
if (sr != null)
replacement = sr; // 如果后继节点的右子树不为空, 那么可以理解为删除一个度为 1的根节点场景
else
replacement = p; // 如果后继节点的右子树为空, 那么可以理解为删除一个度为0的根节点场景
} //if
else if (pl != null)
replacement = pl; //待删除节点的度为 1
else if (pr != null)
replacement = pr; //待删除节点的度为 1
else
replacement = p; //待删除节点的度为 0
/**
* 如果在移动后, 原后继节点(现待删除节点)的度为 1, 让replacement替换待删除节点p, 准备删除节点p
*/
if (replacement != p) {
TreeNode pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
/**
* 移除操作: 将待删除节点p的所有引用全部删除
*/
p.left = p.right = p.parent = null;
}
/**
* 如果待删除节点的颜色是红色, 不需要调整红黑树的性质
*/
TreeNode r = p.red ? root : balanceDeletion(root, replacement);
/**
* 删除待删除节点为度为0的节点
*/
if (replacement == p) { // detach
TreeNode pp = p.parent; //获取待删除节点父节点
/**
* 移除操作, 移除删除节点的parent引用和父节点的左子引用或者右子引用关系
*/
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
//移动根节点到hash桶中
moveRootToFront(tab, r);
}
注意: 如果对红黑树不太理解可以先看着两篇播客(学习红黑树一定要先学会b树)
1. B-树
2. 红黑树
/**
* 左旋转
* 左旋转后发生了什么?
*
* pp pp
* p ==> r
* r p rr
* rl rr rl
*/
static TreeNode rotateLeft(TreeNode root, TreeNode p) {
TreeNode r, pp, rl; //r:p的右子节点, pp:p的父节点, rl: 右子节点的左子节点, rr:r节点的右子节点, 在旋转过程中, 其引用并未发生变化
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null) //把r节点的左子节点移动到p的右子节点位置
rl.parent = p; //如果rl节点不为null, 那么需要维护一下rl的parent引用
if ((pp = r.parent = p.parent) == null) //将r节点的父节点更新为p的父节点pp, 如果p节点为根节点,那么需要染为黑色
(root = r).red = false; //r染为黑色
else if (pp.left == p) //如果p节点是左子节点, 那么pp节点的左子节点更新为r节点
pp.left = r;
else
pp.right = r;
r.left = p; //更新r节点左子节点为p
p.parent = r; //更新p的父节点为r
}
return root;
}
/**
* 右旋转
* 右旋转后发生了什么?
*
* pp pp
* p ==> l
* l ll p
* ll lr lr
*/
static TreeNode rotateRight(TreeNode root, TreeNode p) {
TreeNode l, pp, lr; //l:p的右节点, pp:p的父节点, lr: l的右节点, ll:l的左节点, 因为在整个旋转过程中引用关系并没有发生改变
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null) //如果lr节点不为null, 那么lr节点将作为p的左节点
lr.parent = p; //如果lr不为null, 那么更新lr的parent属性
if ((pp = l.parent = p.parent) == null) //将l节点的更新为p的父节点
(root = l).red = false; //如果p为根节点, 那么l节点需要置为黑色
else if (pp.right == p) //判断p为左子节点还是右子节点, 更新pp的left或者right属性
pp.right = l;
else
pp.left = l;
l.right = p; //更新l的right属性
p.parent = l; //更新p的parent属性为l节点
}
return root;
}
/**
* 在红黑树插入节点后, 修复红黑树的性质
* root: 红黑树的新节点, x为新插入的节点
*
*【红黑树添加节点的12种情况】
* 一些节点的定义:
* x:新增节点
* xp:新增节点的父节点
* xu: 新增节点的叔叔节点
* xpp:新增节点的祖父节点
* xppl:新增节点的祖父节点的左子节点 (新增节点的父节点或者叔叔节点)
* xppr:新增节点的祖父节点的右子节点 (新增节点的父节点或者叔叔节点)
*
* 【1】 xp节点为黑色, xp节点度为 0, x节点为左子节点 ==> 无需调整
* 【2】 xp节点为黑色, xp节点度为 0, x节点为右子节点 ==> 无需调整
* 【3】 xp节点为黑色, xp节点度为 1, x节点为左子节点 ==> 无需调整
* 【4】 xp节点为黑色, xp节点度为 1, x节点为右子节点 ==> 无需调整
* 注意:这里的xp节点为旋转之后的xp节点, x节点为旋转之后的x节点
* 【5】 xp节点为红色, xp节点度为 0, xp节点为(左)子节点, xpp节点为黑色, xpp节点度为 (1), x节点为(左)子节点 ==> xp节点右旋转, xp染为黑色, xpp染为红色
* 【6】 xp节点为红色, xp节点度为 0, xp节点为(左)子节点, xpp节点为黑色, xpp节点度为 (1), x节点为(右)子节点 ==> xp节点先左旋转, 再右旋转, x染为黑色, xpp染为红色
* 【7】 xp节点为红色, xp节点度为 0, xp节点为(右)子节点, xpp节点为黑色, xpp节点度为 (1), x节点为(左)子节点 ==> xp节点先右旋转, 再左旋转, x染为黑色, xpp染为红色
* 【8】 xp节点为红色, xp节点度为 0, xp节点为(右)子节点, xpp节点为黑色, xpp节点度为 (1), x节点为(右)子节点 ==> xp节点左旋转, xp染为黑色, xpp染为红色
*
* 【9】 xp节点为红色, xp节点度为 0, xp节点为(左)子节点, xpp节点为黑色, xpp节点度为 (2), x节点为(左)子节点 ==> xp染为黑色, xu染为黑色, xpp染为红色, 然后以 xpp为新增节点x继续修复红黑树性质
* 【10】 xp节点为红色, xp节点度为 0, xp节点为(左)子节点, xpp节点为黑色, xpp节点度为 (2), x节点为(右)子节点 ==> xp染为黑色, xu染为黑色, xpp染为红色, 然后以 xpp为新增节点x继续修复红黑树性质
* 【11】 xp节点为红色, xp节点度为 0, xp节点为(右)子节点, xpp节点为黑色, xpp节点度为 (2), x节点为(左)子节点 ==> xp染为黑色, xu染为黑色, xpp染为红色, 然后以 xpp为新增节点x继续修复红黑树性质
* 【12】 xp节点为红色, xp节点度为 0, xp节点为(右)子节点, xpp节点为黑色, xpp节点度为 (2), x节点为(右)子节点 ==> xp染为黑色, xu染为黑色, xpp染为红色, 然后以 xpp为新增节点x继续修复红黑树性质
*
* ##为什么没有父节点为黑色, 父节点度为 2的情况 ?
* 因为如果父节点为2, 那么还哪还有位置插入新的节点... 红黑树也是一个二叉树
*
* ##为什么没有父节点为红色, 父节点度不为 0 的情况 ?
* 因为如果父节点为红色,且度不为 0, 那么这棵红黑树将不满足性质4 "RED节点的子节点都是BLACK"
* 具体原因可以按照红黑树推导4阶B树分析:
* 如果父节点为红色, 那么该节点将跟它的黑色节点合为一个B树节点, 如果 父节点为红色,且度不为 0, 那么红黑树将无法推导为一个标准的4阶B树
*/
static TreeNode balanceInsertion(TreeNode root, TreeNode x) {
/**
* 默认插入的新节点为红色
* 为什么是红色呢 ?
* 因为默认为红色,可以满足红黑树五大性质中的四条,"RED节点的子节点都是BLACK" 这条除外
*/
x.red = true;
for (TreeNode xp, xpp, xppl, xppr;;) {
/**
* xp:新增节点的父节点
* xu:新增节点的叔叔节点
* xpp:新增节点的祖父节点
* xppl:新增节点的祖父节点的左子节点 (新增节点的父节点或者叔叔节点)
* xppr:新增节点的祖父节点的右子节点 (新增节点的父节点或者叔叔节点)
*/
if ((xp = x.parent) == null) {
x.red = false; //如果新增节点的父节点为空, 那么新增节点为根节点, 红黑树中根节点必须为黑色, 直接返回即可
return x;
}
/**
* 添加节点不为根节点情况
*/
else if (!xp.red || (xpp = xp.parent) == null)
/**
* xp节点为黑色, 或者父节点为root节点(黑色), 不需要调整
* 情况:【1】【2】【3】【4】
*/
return root;
if (xp == (xppl = xpp.left)) {
/**
* xp节点为左子节点
*/
if ((xppr = xpp.right) != null && xppr.red) {
/**
* [xp节点为左子节点], xpp节点的度为 2, xu节点为红色
* 注意: 此时xu节点必为红色, 如果为黑色,则该红黑树将不满则性质"从任一节点到叶子节点的所有路径都包含相同数目的 BLACK节点"
* 情况:【9】【10】
*/
xppr.red = false; //xu节点染为黑色
xp.red = false; //父节点染为黑色
xpp.red = true; // 祖父节点染为红色
x = xpp; // 将xpp节点当做新增节点,调整红黑树平衡 (4阶B树中的上溢)
}
else {
/**
* [xp节点为左子节点], xpp节点的度为 1
* 情况【6】
*/
if (x == xp.right) {
/**
* [xp节点为左子节点, xpp节点的度为 1], x节点为右子节点(1) ==>左旋转
*/
root = rotateLeft(root, x = xp); //先进行左旋转
//左旋之后,需要更新xp和xpp的引用节点为: 现xp节点为原x节点, 现x节点为原xp节点, xpp节点为现xp节点(原x节点)的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
/**
* [xp节点为左子节点, xpp节点的度为 1], x节点为左子节点
* 情况:【5】
* [xp节点为左子节点, xpp节点的度为 1], x节点为右子节点(2) ==>右旋转
* 情况:【6】
*/
xp.red = false; //xp节点(原x节点)染为黑色
if (xpp != null) {
xpp.red = true; //xpp节点染为红色
root = rotateRight(root, xpp); //再进行右旋转
}
}
} //else
}
/**
* xp节点为右子节点
*/
else {
if (xppl != null && xppl.red) {
/**
* [xp节点为右子节点], xpp节点度为 2, xu节点为红色
* 情况【11】【12】
*/
xppl.red = false; //xu节点染为黑色
xp.red = false; //xp节点然为黑色
xpp.red = true; //xpp染为红色
x = xpp; // 将xpp节点当做新增节点,调整红黑树平衡 (4阶B树中的上溢)
}
else {
/**
* [xp节点为右子节点], xpp节点度为 1
*/
if (x == xp.left) {
/**
* [xp节点为右子节点, xpp节点度为 1], x节点为左子节点(1)
*/
root = rotateRight(root, x = xp); //右旋转
//右旋之后,需要更新xp和xpp的引用节点为: 现xp节点为原x节点, 现x节点为原xp节点, xpp节点为现xp节点(原x节点)的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
/**
* [xp节点为右子节点, xpp节点度为 1], x节点为右子节点
* 情况:【8】
* [xp节点为右子节点, xpp节点度为 1], x节点为左子节点(2)
* 情况:【7】
*/
xp.red = false; // xp染为黑色
if (xpp != null) {
xpp.red = true; //xpp染为红色
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 在红黑树删除节点后, 修复红黑树
*/
static TreeNode balanceDeletion(TreeNode root, TreeNode x) {
/**
* x: 待移除节点被删除后的替换节点
* xp: 父节点
* xpl: 父节点的左子节点
* xpr: 父节点的右子节点
* 【当成程序执行到这里】: 如果待删除节点是度不为0的节点, 那么此节点已经被删除, 而顶替节点也已经替代了已经删除节点, 由于此时红黑树可能已经失去平衡, 所以需要进行维护
*/
for (TreeNode xp, xpl, xpr;;) {
if (x == null || x == root)
/**
* 如果x节点现在是根节点, 则无需调整
*/
return root;
else if ((xp = x.parent) == null) {
/**
* 替换节点为根节点, 染为黑色
*/
x.red = false;
return x;
}
/**
* 【删除场景 1】:被删除节点存在一个红色的子节点,在删除后, 该子节点替代被删除节点的位置, 所以改为黑色即可
* 如果替换节点为红色; 那么仅需要染为黑色, 红黑树就达到了平衡, 退出循环
*/
else if (x.red) {
x.red = false;
return root;
}
/**
* [x节点为左子点] 因为x节点所处位置不一样, 后面的旋转方式也不一样
* 【删除场景 2】:被删除节点为黑色叶子节点 (即:x节点的度为0, 不存在可以替换的子节点)
* 【程序执行到这里】: 此时x节点还没有删除, 需要先调整红黑树性质, 再删除
*/
else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
/**
* 【删除场景 2.1】:被删除节点为黑色叶子节点, 并且兄弟节点为红色, 需要将兄弟节点转为黑色
*/
xpr.red = false; //兄弟节点染为黑色
xp.red = true; //父节点染为红色
root = rotateLeft(root, xp); //在经过左旋转后, x,xp节点的情况为:x节点为左子节点, xp节点度为 2, 兄弟节点为黑色
xpr = (xp = x.parent) == null ? null : xp.right;
}
/**
* 【删除场景 2.2】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色
*/
if (xpr == null)
x = xp;
else {
TreeNode sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) && (sl == null || !sl.red)) {
/**
* 【删除场景 2.2.1】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色,兄弟节点 没有 红色子节点
*/
xpr.red = true; //兄弟节点染为红色, 第一次执行到此处时:不用更新x(p)节点的颜色, 后面p节点会被删除
x = xp; // 把xp赋给x, 把xp当做被删除的节点, 继续向上修复红黑树 (基于4阶B树,出现下溢情况)
}
else {
/**
* 【删除场景 2.2.2】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色,兄弟节点 有 红色子节点
*/
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false; //如果兄弟节点的另一个左子节点不为空, 染为黑即可, 在右旋转之后, sl节点将作为新xp节点的左子节点
xpr.red = true; //兄弟节点染红, 因为兄弟节点会替代原xp节点, 而原xp节点为红色
root = rotateRight(root, xpr); //右旋转
xpr = (xp = x.parent) == null ? null : xp.right;
}
/**
* 兄弟节点不为空,则兄弟节点需要继承原xp节点的颜色
*/
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
/**
* 如果原xp节点不为null, 则需要将原xp节点染为黑色
*/
if (xp != null) {
xp.red = false;
/**
* 如果兄弟节点有两个红色节点, 那么还需要一次右旋,恢复平衡
*/
root = rotateLeft(root, xp);
}
x = root;
}
}
}
/**
* [x节点为右子点] 因为x节点所处位置不一样, 后面的旋转方式也不一样
* 【删除场景 2】:被删除节点为黑色叶子节点 (即:x节点的度为 0, 不存在可以替换的子节点)
* 【程序执行到这里】: 此时x节点还没有删除, 需要先调整红黑树性质, 再删除
*/
else { // symmetric
/**
* 【删除场景 2.1】:被删除节点为黑色叶子节点, 并且兄弟节点为红色, 需要将兄弟节点转为黑色
*/
if (xpl != null && xpl.red) {
xpl.red = false; //兄弟节点染为黑色
xp.red = true; //父节点染为红色
root = rotateRight(root, xp); //以xp进行右旋转, 旋转之后, 原兄弟节点为原xp节点的父节点
xpl = (xp = x.parent) == null ? null : xp.left;
}
/**
* 【删除场景 2.2】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色
*/
if (xpl == null)
x = xp;
else {
TreeNode sl = xpl.left, sr = xpl.right; //sl:兄弟节点的左子节点, sr:兄弟节点右子节点
if ((sl == null || !sl.red) && (sr == null || !sr.red)) {
/**
* 【删除场景 2.2.1】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色,兄弟节点 没有 红色子节点
*/
xpl.red = true; //兄弟节点染为红色, 第一次执行到此处时:不用更新x(p)节点的颜色, 后面p节点会被删除
x = xp; // 把xp赋给x, 把xp当做被删除的节点, 继续向上修复红黑树 (基于4阶B树,出现下溢情况)
}
else {
/**
* 【删除场景 2.2.2】:被删除节点为黑色叶子节点, 并且兄弟节点为黑色,兄弟节点 有 红色子节点
* [x节点为右子点]
*/
if (sl == null || !sl.red) { //兄弟节点的右子节点为红色 (根据【删除场景 2.2.1】条件推算)
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl); //左旋转, sr节点替环xp节点, xp节点替换x节点 (在旋转后:sr为父节点,xp为右子节点, sl为左子节点)
xpl = (xp = x.parent) == null ? null : xp.left; //更新xpl节点
}
/**
* 兄弟节点不为空,则兄弟节点需要继承原xp节点的颜色
*/
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false; //sl节点染为黑色
}
/**
* 如果原xp节点不为null, 则需要将原xp节点染为黑色
*/
if (xp != null) {
xp.red = false;
/**
* 如果兄弟节点有两个红色节点, 那么还需要一次右旋,恢复平衡
*/
root = rotateRight(root, xp);
}
x = root;
}//else
}
}
} //for
}
检查红黑树: checkInvariants
/**
* 验证红黑树的准确性
*/
static boolean checkInvariants(TreeNode t) {
// tp:父节点, tl:左子节点, tr:右子节点, tb:前驱节点, tn:后继节点
TreeNode tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode)t.next;
/**
* 当出现以下任一情况时, 红黑树或者双向链表不正确
*/
/**
* 1、如果前驱节点存在, 但是前驱节点的后继节点不是当前节点
*/
if (tb != null && tb.next != t)
return false;
/**
* 2、如果后继节点存在, 但是后继节点的前驱节点不是当前节点
*/
if (tn != null && tn.prev != t)
return false;
/**
* 3、父节点存在, 但是父节点的左子节点、右子节点均不是当前节点
*/
if (tp != null && t != tp.left && t != tp.right)
return false;
/**
* 4、左子节点存在, 但是左子节点的父节点不是当前节点或者左子节点的hash值大于当前节点的hash值
*/
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
/**
* 5、右子节点存在, 但是右子节点的父节点不是当前节点或者右子节点的hash值小于当前节点的hash值
*/
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
/**
* 6、当前节点是红色,孩子节点也是红色 ==>红黑树性质
*/
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;
/**
* 通过验证, 返回true
*/
return true;
}
}