文章目录
1 简介
Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:
下面针对各个实现类的特点做一些说明:
(1) HashMap:
-
HashMap是基于哈希表实现的,每一个元素是一个key-value对,它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
-
HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
-
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
-
HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆
-
Java8中又对此类底层实现进行了优化,比如引入了红黑树的结构、 优化哈希冲突算法hash() 以解决哈希碰撞
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
2 源码分析
2.1 成员变量
//HashMap继承自AbstractMap,实现了Map接口,Map接口定义了所有Map子类必须实现的方法。AbstractMap也实现了Map接口,并且提供了两个实现Entry的内部类:SimpleEntry和SimpleImmutableEntry。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
//默认的初始容量,必须是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子,默认值为0.75,如果实际元素所占容量占分配容量的75%时就要扩容了。如果填充比很大,说明利用的空间很多,但是查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),HashMap本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。
static final float _LOAD_FACTOR = 0.75f;
//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;
//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
//存储数据的Entry数组,长度是2的幂。
transient Entry[] table;
//
transient Set<Map.Entry<K,V>> entrySet;
//map中保存的键值对的数量
transient int size;
//需要调整大小的极限值(容量*装载因子)
int threshold;
//装载因子
final float loadFactor;
//map结构被改变的次数
transient volatile int modCount;
重要参数:
- 加载因子(_LOAD_FACTOR):loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。 - Threshold: threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
- TREEIFY_THRESHOLD: 一个桶的树化阈值
当桶中元素个数超过这个值时,需要判断是否使用红黑树节点替换链表节点,这个值必须为 8,要不然频繁转换效率也不高 - UNTREEIFY_THRESHOLD: 一个树的链表还原阈值
当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构(因为在扩容后,每个元素的位置需要重新计算,扩容后新建的树的高度不一定还大于8),这个值应该比上面那个小,至少为 6,避免频繁转换 - MIN_TREEIFY_CAPACITY: 哈希表的最小树形化容量
当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则会通过扩容解决碰撞,而不是树形化,为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD - DEFAULT_INITIAL_CAPACITY: 哈希桶数组table的长度length大小必须为2^n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。具体原因参考:https://blog.csdn.net/weixin_42615068/article/details/103022760 , 简单说就是在利用哈希值求取位置时(
(n-1) & hash
),才能最大限度的利用 hash 值,并更好的散列,只有全是1,才能有更多的散列结果。--------n-1的二进制为111111 - modCount: Fast-fail策略标记
关于Fast-fail:
1 失败原因:
在使用迭代器的过程中如果HashMap被修改,那么ConcurrentModificationException将被抛出,也即Fast-fail策略。
当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出ConcurrentModificationException。该Iterator的remove方法也会做类似的检查。该异常的抛出意在提醒用户及早意识到线程安全问题。
2 线程安全解决方案
单线程条件下,为避免出现ConcurrentModificationException,需要保证只通过HashMap本身或者只通过Iterator去修改数据,不能在Iterator使用结束之前使用HashMap本身的方法修改数据。因为通过Iterator删除数据时,HashMap的modCount和Iterator的expectedModCount都会自增,不影响二者的相等性。如果是增加数据,只能通过HashMap本身的方法完成,此时如果要继续遍历数据,需要重新调用iterator()方法从而重新构造出一个新的Iterator,使得新Iterator的expectedModCount与更新后的HashMap的modCount相等。
多线程条件下,可使用Collections.synchronizedMap方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。
2.2 存储结构
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。
HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。该数组中或者存放Node对象(即单链表)或者存放TreeNode对象(即红黑树)
Node源码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode源码
可以看出出真正的维护红黑树结构的方法并没有在HashMap中,全部都在TreeNode类内部。关于红黑树的理解可参考:https://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
//根据他的构造函数向上追溯TreeNode<K,V>继承了LinkedHashMap.Entry<K,V>而后者又继承了HashMap.Node<K,V>。所以TreeNode依然保有Node的属性,同时由于添加了prev这个前驱指针使得链表变为了双向的。
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V>
{
TreeNode<K, V> parent; // red-black tree links
TreeNode<K, V> left;
TreeNode<K, V> right;
TreeNode<K, V> prev; // needed to unlink next upon deletion 前驱指针是为了让链表变成双向,继承自LinkedHashMap
boolean red;
TreeNode(int hash, K key, V val, Node<K, V> next)
{
super(hash, key, val, next);
}
final void treeify(Node<K,V>[] tab)
{
// ......
}
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x)
{
// ......
}
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p)
{
// ......
}
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p)
{
// ......
}
// ......其余方法省略
}
2.3 构造方法
/**
*使用默认的容量及装载因子构造一个空的HashMap
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 根据给定的初始容量和装载因子创建一个空的HashMap
* 初始容量小于0或装载因子小于等于0将报异常
*/
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);
}
/**
*根据指定容量创建一个空的HashMap
*/
public HashMap(int initialCapacity) {
//调用上面的构造方法,容量为指定的容量,装载因子是默认值
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap提供了四种构造方法:
(1)使用默认的容量及装载因子构造一个空的HashMap;
(2)根据给定的初始容量和装载因子创建一个空的HashMap;
(3)根据指定容量创建一个空的HashMap;
(4)通过传入的map创建一个HashMap。
putMapEntries方法是一个final方法,不可以被修改,该方法实现了将另一个Map的所有元素加入表中,参数evict初始化时为false,其他情况为true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) {
//根据m的元素数量和当前表的加载因子,计算出阈值
float ft = ((float)s / loadFactor) + 1.0F;
//修正阈值的边界 不能超过MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
//如果新的阈值大于当前阈值
if (t > threshold)
//返回一个>=新的阈值的 满足2的n次方的阈值
threshold = tableSizeFor(t);
}
//如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。
else if (s > threshold)
resize();
//遍历 m 依次将元素加入当前表中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
其中,涉及到两个操作,一个是计算新的阈值,另一个是扩容方法:
1)tableSizeFor:tableSizeFor方法保证函数返回值是大于等于给定参数initialCapacity最小的2的幂次方的数值
static final int tableSizeFor(int cap) {
//经过下面的 或 和位移 运算, n最终各位都是1。
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//判断n是否越界,返回 2的n次方作为 table(哈希桶)的阈值
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
int n = cap - 1
给定的cap 减 1,为了避免参数cap本来就是2的幂次方,这样一来,经过后续操作,cap将会变成2 * cap,是不符合我们预期的
假设cap=20
2.4 扩容方法resize()
2)如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。这涉及到了扩容方法resize:
扩容流程:
/**
* 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容
*/
final Node<K,V>[] resize() {
//oldTab 为当前表的哈希桶
Node<K,V>[] oldTab = table;
//当前哈希桶的容量 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前的阈值
int oldThr = threshold;
//初始化新的容量和阈值为0
int newCap, newThr = 0;
//如果当前容量大于0
if (oldCap > 0) {
//针对情况2:若扩容前的数组容量超过最大值,则不再扩充
if (oldCap >= MAXIMUM_CAPACITY) {
//则设置阈值是2的31次方-1
threshold = Integer.MAX_VALUE;
//同时返回当前的哈希桶,不再扩容
return oldTab;
}
// 针对情况2:若无超过最大值,就扩充为原来的2倍,注意Thrershold同时扩大为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧的容量大于等于默认初始容量16
//那么新的阈值也等于旧的阈值的两倍
newThr = oldThr << 1; // double threshold
}
// 针对情况1:初始化哈希表(采用指定 or 默认值)
//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0)
newCap = oldThr;//那么新表的容量就等于旧的阈值(将threshold设置为newCap,所以要用tableSizeFor方法保证threshold是2的幂次方
else {
//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况
newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16
//新的阈值为默认容量16 * 默认加载因子0.75f = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况(有默认阈值初始化),需要重新计算新的Threshold
float ft = (float)newCap * loadFactor;//根据新表容量 和 加载因子 求出新的阈值
//进行越界修复
newThr = (newCap < MAXIMUM_CAPACITY && ft <(float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据新的容量 构建新的哈希桶
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//更新哈希桶引用
table = newTab;
//如果以前的哈希桶中有元素
//下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
if (oldTab != null) {
//遍历老的哈希桶
for (int j = 0; j < oldCap; ++j) {
//取出当前的节点 e
Node<K,V> e;
//如果当前桶中有元素,则将链表赋值给e
if ((e = oldTab[j]) != null) {
//将原哈希桶置空以便GC
oldTab[j] = null;
//如果当前链表中就一个元素,(没有发生哈希碰撞)
if (e.next == null)
//直接将这个元素放置在新的哈希桶里。
//注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
//如果桶中元素是树节点,会根据UNTREEIFY_THRESHOLD (默认为 6),将桶中的树形结构缩小或者直接还原(切分)为链表结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
else {
//因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位,或者扩容后的下标,即high位。high位=low位+原哈希桶容量
//低位链表的头结点、尾节点
Node<K,V> loHead = null, loTail = null;
//高位链表的头节点、尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;//临时节点 存放e的下一个节点
do {
next = e.next;
//利用位运算代替常规运算:利用哈希值与旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
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);
//将低位链表存放在原index处
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链表存放在新index处
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上述resize()方法中几个知识点的理解:
第一点:求取坐标位置(n-1) & hash
关于哈希扰动函数hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode()出来的散列值是不能直接拿来用的,用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标
源码中模运算就是把散列值和数组长度做一个"与"操作,这也正好解释了为什么HashMap的数组长度要取2的整次幂
因为这样(数组长度-1)正好相当于一个“低位掩码”
“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问
例如:
以初始长度16为例,16-1=15
和某散列值做“与”操作如下,结果就是截取了最低的四位值
但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重
加入扰动函数后:
右位移16位,正好是32位一半,自己的高半区和低半区做异或,就是为了混合原始hashCode的高位和低位,以此来加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
-
若e.hash值只用自身的hashcode,index只会和e.hash的低位做&操作.这样一来,index的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险
-
加入扰动函数后,通过高位右移和异或操作混合hashCode值的高位和低位信息,增加低位的随机性,同时保留高位部分信息,减少哈希冲突的风险。
第二点:元素复制时关于高位和低位选择问题(e.hash & oldCap)
假设原来Node[]容量为16(图a),扩容后容量为32 (图b)
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,对于扩容后数据应该是存放在高位还是低位,只需要看看原来的hash值新增的那个bit是1还是0就好了(对应于程序
e.hash & oldCap
),是0的话索引没变,存放在低位,是1的话索引变成“原索引+oldCap”,存放在高位。直观图如下图所示:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
第三点:((TreeNode<K,V>)e).split(this, newTab, j, oldCap)
如果桶中当前位置的元素为树节点,在重新复制时将当前树分裂成低位和高位的两颗树,如果新树的节点小于UNTREEIFY_THRESHOLD,需要将其转化为链表,该函数只有在resize()中用。
/**
* 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.
* 将树从给定的结点分裂成低位和高位的两棵树,若新树结点太少则转为线性链表。只有resize时会调用
*
* @param map the map
* @param tab the table for recording bin heads存储链表头的hash表
* @param index the index of the table being split需要分裂的表下标位置
* @param bit the bit of hash to split on分裂时分到高位和低位的依据参数,实际使用时输入的是扩展之前旧数组的大小
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;//低位头尾指针
TreeNode<K,V> hiHead = null, hiTail = null;//高位头尾指针
int lc = 0, hc = 0;//低位和高位的结点个数统计
for (TreeNode<K,V> e = b, next; e != null; e = next) {//e从this开始遍历直到next为null
next = (TreeNode<K,V>)e.next;
e.next = null;
//这段决定了该结点被分到低位还是高位,依据算式是e.hash mod bit,由于bit是扩展前数组的大小,所以一定是2的指数次幂,所以bit一定只有一个高位是1其余全是0
//这个算式实际是判断e.hash新多出来的有效位是0还是1,若是0则分去低位树,是1则分去高位树
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) //若高位树为null则代表整棵树全保留在了低位,树没有变化所以不用进行后面的treeify
loHead.treeify(tab);
}
}
if (hiHead != null) {//这段与上面对于低位部分的分析相对应
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;//高位所处的位置为原本位置+旧数组的大小即bit
if (loHead != null)
hiHead.treeify(tab);
}
}
}
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;//hd是头部,tl是尾部
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);//根据q产生一个新的Node结点,next=null,hash key value和q相等
if (tl == null)
hd = p;//第一个结点产生时头部指向它
else
tl.next = p;
tl = p;
}
return hd;
}
HashMap.Node<K, V> replacementNode(HashMap.Node<K, V> var1, HashMap.Node<K, V> var2) {
return new HashMap.Node(var1.hash, var1.key, var1.value, var2);
}
树形化函数treeify
将在后面红黑树操作中详细解析
扩容方法总结
谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?
调用场景:
1.初始化数组table
2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中
实现过程:(细讲)
1.通过判断旧数组的容量是否大于0来判断数组是否初始化过
否:进行初始化
判断是否调用无参构造器,
是:使用默认的大小和阙值
否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数
是,进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中
概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
2.5 成员方法
2.5.1 put ()方法
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
//向哈希表中添加元素
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) {
//tab存放当前的哈希桶,p用作临时链表节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//@1 如果当前哈希表是空的,则创建,代表是初始化
if ((tab = table) == null || (n = tab.length) == 0)
//那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
n = (tab = resize()).length;
//@2如果当前index的节点是空的,表示没有发生哈希碰撞。直接构建一个新节点Node,挂载在index处即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//否则 发生了哈希冲突。
Node<K,V> e; K k;
//@3如果哈希值相等,key也相等,则是覆盖value操作
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;//将当前节点引用赋值给e
//@4判断该位置节点为红黑树
else if (p instance of TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//@5该位置节点为链表
else {//不是覆盖操作,则插入一个普通链表节点
//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//遍历到尾部,追加新节点到尾部
p.next = newNode(hash, key, value, null);
//如果追加节点后,链表数量>=8,则转化为红黑树
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;
}
}
//如果e不是null,说明有需要覆盖的节点,
if (e != null) { // existing mapping for key
//则覆盖节点值,并返回原oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeAccess(e);
return oldValue;
}
}
//如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。
++modCount;
//更新size,超过最大容量需扩容。
if (++size > threshold)
resize();
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeInsertion(evict);
return null;
}
put方法总结:
1.如果散列表为空时,调用resize()初始化散列表
2.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
3.如果当前位置没有元素,即没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(哈希值相同),进行三种判断
4.1:若key的哈希值相同或者equals后内容相同,则替换旧值
4.2:如果是红黑树结构,就调用树的插入方法
4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则resize进行扩容
2.5.2 get()方法
public V get(Object key) {
Node<K,V> e;
//传入扰动后的哈希值 和 key 找到目标节点Node
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//传入扰动后的哈希值 和 key 找到目标节点Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//查找过程,找到返回节点,否则返回null
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
中间涉及到的getTreeNode方法
getTreeNode这个方法在HashMap中被多次使用,左右是寻找某个结点所在的树中是否有hash和key值符合的结点。我们可以看到这个方法一定会确保最后调用的是root.find(),也就是说find方法调用时this一定是根结点。所以无论最初调用getTreeNode的结点在树中处于什么位置,最后都会从根结点开始寻找,由于红黑树是相对平衡的二叉搜索树,所以可以认为搜索时间相比于链表从O(n)下降到了O(lgn)
/**
* Calls find for root node.从根结点寻找h和k符合的结点
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
* 从根结点p开始根据hash和key值寻找指定的结点。kc是key的class
* 注意在比较哈希值时
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;//该方法调用时this是根结点
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;//p.hash>参数hash时,移向左子树
else if (ph < h)
p = pr;//p.hash<参数hash时,移向右子树
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;//p.hash=参数hash,且p.key与参数key相等找到指定结点并返回
else if (pl == null)//若hash相等但key不等,向左右子树非空的一侧移动
p = pr;
else if (pr == null)
p = pl;
//哈希值相等,键值不为null比较键值
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&//kc是否是一个可比较的类
(dir = compareComparables(kc, k, pk)) != 0)//比较k和p.key
p = (dir < 0) ? pl : pr;//k<p.key向左子树移动否则向右子树移动
else if ((q = pr.find(h, k, kc)) != null)//这里开始的条件仅当输入k=null的时候才会进入,先检查右子树再检查左子树
return q;
else
p = pl;
} while (p != null);
return null;
}
root()方法可以返回根结点,实现很简单就是不断从一个结点检查parent是否为null
/**
* Returns root of tree containing this node.返回根结点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;//不断检查parent是否为null,为null的是根结点
}
}
补充:
find函数中comparableClassFor和compareComparables
Class<?> comparableClassFor(Object x):当x的类型为X,且X直接实现了Comparable接口(比较类型必须为X类本身)时,返回x的运行时类型;否则返回null。具体参考:https://blog.csdn.net/qpzkobe/article/details/79533237
直观理解:
/**
* Returns k.compareTo(x) if x matches kc (k's screened comparable
* class), else 0.
* 如果x的类型是kc,返回k.compareTo(x)的比较结果
* 如果x为空,或者类型不是kc,返回0
*/
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
get()方法总结:
对key的hashCode进行hashing,得到的哈希值与数组长度减一进行与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,查找的判断条件是:e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))),在比较hash值的同时需要比较key的值是否相同。
2.5.3 remove()方法
removeNode (key)的返回结果应该是被移除的元素,如果不存在这个元素则返回为null
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) {
// p 是待删除节点的前置节点
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果哈希表不为空,则根据hash值算出的index下 有节点的话。
if ((tab = table) != null && (n = tab.length) > 0&&(p = tab[index = (n - 1) & hash]) != null) {
//node是待删除节点
Node<K,V> node = null, e; K k; V v;
//如果链表头的就是需要删除的节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;//将待删除节点引用赋给node
else if ((e = p.next) != null) {//否则循环遍历 找到待删除节点,赋值给node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash && ((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果有待删除节点node, 且 matchValue为false,或者值也相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)//如果node == p,说明是链表头是待删除节点
tab[index] = node.next;
else//否则待删除节点在表中间
p.next = node.next;
++modCount;//修改modCount
--size;//修改size
afterNodeRemoval(node);//LinkedHashMap回调函数
return node;
}
}
return null;
}
2.5.4 contains() and clear()
HashMap没有提供判断元素是否存在的方法,只提供了判断Key是否存在及Value是否存在的方法,分别是containsKey(Object key)、containsValue(Object value)。
containsKey(Object key)方法很简单,只是判断getNode (key)的结果是否为null,是则返回false,否返回true。
判断一个value是否存在比判断key是否存在还要简单,就是遍历所有元素判断是否有相等的值。这里分为两种情况处理,value为null何不为null的情况,但内容差不多,只是判断相等的方式不同。这个判断是否存在必须遍历所有元素,是一个双重循环的过程,因此是比较耗时的操作。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
//遍历哈希桶上的每一个链表
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
//如果找到value一致的返回true
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
clear()方法删除HashMap中所有的元素,这里就不用一个个删除节点了,而是直接将table数组内容都置空,这样所有的链表都已经无法访问,Java的垃圾回收机制会去处理这些链表。table数组置空后修改size为0。
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
3 HashMap红黑树树化过程
红黑树的理论可参考:https://algs4.cs.princeton.edu/33balanced/ 或者https://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
- 一个节点标记为红色或者黑色。
- 根是黑色的。
- 如果一个节点是红色的,那么它的子节点必须是黑色的(这就是为什么叫红黑树)。
- 一个节点到到一个null引用的每一条路径必须包含相同数目的黑色节点(所以红色节点不影响)
3.1 红黑树的成员变量
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; //左节点
TreeNode<K,V> right; //右节点
TreeNode<K,V> prev; // 在链表中的前一个节点(主要由于继承LinkedHashMap)
boolean red; //染红或者染黑标记
3.2 树化函数treeifyBin()
桶的树形化 treeifyBin(),如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。这个替换的方法叫 treeifyBin() 即树形化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//将桶内所有的 链表节点 替换成 红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前哈希表为空,或者哈希表中元素的个数小于进行树形化的阈值(默认为 64),就去新建/扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//如果哈希表中的元素个数超过了树形化阈值,进行树形化,e是哈希表中指定位置桶里的链表节点,从第一个开始
else if ((e = tab[index = (n - 1) & hash]) != null) {
//新建一个树形节点,内容和当前链表节点e一致
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
//只是对桶里面的每个元素调用了replacementTreeNode方法将当前的节点变为一个树形节点:
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
在所有的节点都替换成树形节点后需要让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了,之前的操作并没有设置红黑树的颜色值,现在得到的只能算是个二叉树。在最后调用树形节点 hd.treeify(tab) 方法进行塑造红黑树
final void treeify(Node<K, V>[] tab)
{
TreeNode<K, V> root = null;
// 以for循环的方式遍历刚才我们创建的链表。
for (TreeNode<K, V> x = this, next; x != null; x = next)
{
// next向前推进。
next = (TreeNode<K, V>) x.next;
x.left = x.right = null;
// 为树根节点赋值。
if (root == null)
{
x.parent = null;
x.red = false;
root = x;
} else
{
// x即为当前访问链表中的项。
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。
// 这里从root开始,是以一个自顶向下的方式遍历添加。
// for循环没有控制条件,由代码内break跳出循环。
for (TreeNode<K, V> p = root;;)
{
// dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。
// ph:parent hash。
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// tieBreakOrder(k, pk)是在插入结点的key值k和父结点的key值pk无法比较出大小时,用于比较k和pk的hash值大小。
else if ((kc == null && (kc = comparableClassFor(k)) == null)
|| (dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// xp:x parent。
TreeNode<K, V> xp = p;
// 找到符合x添加条件的节点。
if ((p = (dir <= 0) ? p.left : p.right) == null)
{
x.parent = xp;
// 如果xp的hash值大于x的hash值,将x添加在xp的左边。
if (dir <= 0)
xp.left = x;
// 反之添加在xp的右边。
else
xp.right = x;
// 维护添加后红黑树的红黑结构。
root = balanceInsertion(root, x);
// 跳出循环当前链表中的项成功的添加到了红黑树中。
break;
}
}
}
}
// Ensures that the given root is the first node of its bin,自己翻译一下。
moveRootToFront(tab, root);
}
第一次循环会将链表中的首节点作为红黑树的根,而后的循环会将链表中的的项通过比较hash值然后连接到相应树节点的左边或者右边,插入可能会破坏树的结构所以接着执行balanceInsertion。
//这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
//这里源码注释也说了,这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
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;
}
补充:
System.identityHashCode(a)和hashCode()的区别:
前者调用Object.hashCode()方法计算哈希值,后者调用对象重写的hashCode()方法。
3.3 平衡插入函数balanceInsertion()
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
{
// 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。
x.red = true;
// 这些变量名不是作者随便定义的都是有意义的。
// xp:x parent,代表x的父节点。
// xpp:x parent parent,代表x的祖父节点
// xppl:x parent parent left,代表x的祖父的左节点。
// xppr:x parent parent right,代表x的祖父的右节点。
for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
{
// 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。
if ((xp = x.parent) == null)
{
x.red = false;
return x;
}
// 进入else说明不是根节点。
// 如果父节点是黑色,那么大吉大利(今晚吃鸡),红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。
// 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色的,添加在后面没有任何问题。
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 一旦我们进入到这里就说明了两件是情
// 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。
// 2.x的祖父节点xpp不为空。
// 判断如果父节点是否是祖父节点的左节点
if (xp == (xppl = xpp.left))
{
// 父节点xp是祖父的左节点xppr
// 判断祖父节点的右节点不为空并且是否是红色的
// 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。
// 这里大家有疑问为什么将x = xpp?
// 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。
// 所以令x = xpp,然后进行下下一层循环,接着往上走。
if ((xppr = xpp.right) != null && xppr.red)
{
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 进入到这个else里面说明。
// 父节点xp是祖父的左节点xppr。
// 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
// 下面要判断x是xp的左节点还是右节点。
else
{
// x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。
// 下面是第一次旋转。
if (x == xp.right)
{
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。
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);
}
}
}
}
}
}
左旋和右旋
//左旋操作,见图中右向左,p是x,r是y
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {//r是p的右儿子,也就是图中的y
if ((rl = p.right = r.left) != null)//r的左儿子β成为p的右儿子
rl.parent = p;
if ((pp = r.parent = p.parent) == null)//p的父亲成为r的父亲
(root = r).red = false;//若p是根结点,r的颜色改黑色
else if (pp.left == p)//r取代p原本的位置
pp.left = r;
else
pp.right = r;
r.left = p;//p成为r的右儿子
p.parent = r;
}
return root;
}
//右旋操作,见图中左向右,p是y,l是x
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {//l是p的左儿子,即图中x
if ((lr = p.left = l.right) != null)//l的右儿子β成为p的左儿子
lr.parent = p;
if ((pp = l.parent = p.parent) == null)//p的父亲成为l的父亲
(root = l).red = false;//若p是根结点,l的颜色改黑色
else if (pp.right == p)//l取代p原本的位置
pp.right = l;
else
pp.left = l;
l.right = p;//p成为l的右儿子
p.parent = l;
}
return root;
}
References
hashMap成员函数等参考:
只看除红黑树部分:https://www.cnblogs.com/winterfells/p/8876888.html
牛客网:https://www.nowcoder.com/discuss/151172
美团:https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=504261609&idx=1&sn=cdc762fe7c9050e7e9a2554d8c3337a4&mpshare=1&scene=1&srcid=&sharer_sharetime=1573641292799&sharer_shareid=f5c58f7a43a8f22a345b50dcd643b1ce&pass_ticket=rMCIxXJivFH2fB4k%2FzWjbivycPweKhI8wXeZ9glwO13FJDHIPO5j2nPufMuIMvp9#rd
红黑树部分参考:
重要https://www.cnblogs.com/finite/p/8251587.html
重要https://www.cnblogs.com/graywind/p/9471756.html
https://blog.csdn.net/u011240877/article/details/53358305
TreeNode成员函数(较全)https://www.jianshu.com/p/91215c6d061f
HashMap面试题:
https://www.cnblogs.com/zengcongcong/p/11295349.html