探索HashMap底层实现
前言
探索HashMap底层实现是基于JDK1.8,看代码之前翻了一下别人写的博客我才知道JDK1.7版本的HashMap是由数组 + 链表的数据结构组成,而对于JDK1.8是由数组 + 链表 + 红黑树的数据结构组成,所以我又去了解了什么是二叉树、平衡二叉树、红黑树,为的就是能做个铺垫。既然是由数组、链表、红黑树组成,加上平时我们了解过的一些细节,可以猜到它的数据结构应该是这样子的,如图所示:

长的虽然有点丑。下面我们就开始探索它吧,还是先从注释开始看!
阅读注释

HashMap的每个节点都由一个键值对组成,也就是一个Key对应着一个Value,相当于绑定关系了,这两者都可以为Null。Hashtable的代码我还没有看过,不过这个结论不用质疑。HashMap是无序的,这跟它的实现机制有关。

简单来说,迭代所需的时间与遍历数组、链表、红黑树的大小成比例,因为数据有可能存放在链表、红黑树中,它要把所有的数据都遍历出来。

当哈希表中元素的填充数量超过加载因子与当前容量的乘积时,就会发生扩容!

为什么较高的加载因子会减少空间开销,增加了查找的成本呢?加载因子表示元素填满的程度,在容量不变的情况下,随着加载因子越大,填满的元素就越多,空间利用率变大了。假设容量固定值为16,有以下情况:
-
A. 加载因子0.75,该数组在不扩容的情况下最多可填充0.75 * 16 = 12。
-
B. 加载因子1,该数组在不扩容的情况下最多可填充1* 16 = 16。
所以我们说空间利用率变大了,同样的,元素多了,查找的成本也就自然增加了。

若是知道键值对的数量,可创建具有指定容量大小的HashMap。

HashMap属于非线程安全,需要在外部进行控制。

Java提供了方法来保证HashMap的线程安全。

该说法很ArrayList很像。

数据结构
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 唯一序列号
private static final long serialVersionUID = 362498820763181265L;
/**
* 默认的初始容量,必须是2的幂次方!!!
*
* 1 << 4 = 16,为什么不直接写16呢?
* 我们都知道计算机底层都是用二进制操作的,而如果直接写16的话最终还是要转换成二进制,对于位运算来说,就是直接用二进制进行计算的,所以效率会更高一些
*
* 为什么是2的幂次方?
* HashMap的底层是由数组 + 链表 + 红黑树组成的,那么先从数组开始存起,先不管规则如何,想要往数组中存入元素,必须要先知道指定索引
* 虽然和ArrayList一样都是数组,但对于HashMap添加一个元素来说,并不是按照顺序存入,那将和ArrayList没什么区别了
* 既然不是按顺序存入,那就是按照一定的规则去计算索引了,当然了,这个规则不能是死的,所以只能拿着传入的值进行一定的规则运算
*
* 因为HashMap可以存入任意类型的元素,不管是Key还是Value,所以对于任意类型来说都应该可以使用这个规则,也就是说它必须存在于任意类型中,不管是方法还是成员变量,那我们能想到的
* 也只有Object了,它是所有类型的父类,加上索引的值应该是int类型的,所以它最终采用了hashCode方法
*
* 不过你会发现hashCode方法返回的数值是不可预测的,而对于HashMap中的数组来说,索引的取值必须要在0~15之间,所以这个规则还没有结束,必须把hashCode的结果控制在0-15之间
* 第一个反应想到的对容量大小采用取余运算,没错,是可以的,但是有更好的方法,就像上面一样,为了提高效率采用了位运算,这里我们采用的位运算是&
*
* 那为什么不用 | 呢?只能说在A | B计算中(A相当于是hashCode,B相当于是容量大小),B没办法对结果产生范围性的控制,比如下面:
*
* A: 1000
* |
* B: 0111
* ----------
* 1111
* B的最大值只能取到7,而结果确实15,肯定不行,所以最终采用了&。
*
* 因为索引的最大值只能取到15,所以是 & 15,如果是16,那么就能取到16造成索引越界了,不过它有一些要求(重点来了),细心的同学会发现在构造HashMap时是可以指定其他值,比如13、17,这些都不是2的幂次方,我们假设采用13
*
* 1000 0010
* & &
* 1101 1101
* ------- -------
* 0000 0000
*
* 你可以多计算几个,我们发现,有些索引它没办法取到,就上面的索引为2它取不到,关键就是因为1101(13)中的第三位是0,不管上面的取值(hashCode)如何,它的结果都是0,所以它的值必须要是1才可以,才能取到所有的值,所以应该是1111这样* 子的一种格式,就是最后几位都是1,不能是0和1发生间隔,而我们刚才说了最多只能取到1111(15),这个值是由容量大小 -1 决定的,所以应该是capacity -1的结果要是1111的这种格式才可以,我们发现:
* capacity = 111 + 1 = 1000 = 8
* capacity = 1111 + 1 = 10000 = 16
* capacity = 11111 + 1 = 100000 = 32
* capacity = 111111 + 1 = 1000000 = 64
*
* 多的就不计算了,这些结果都是2的幂次方,不知道我讲明白了没有...
*
*
* ===========================================================分割线=========================================================================================
*
*
* 既然已经解释那么多,还差一个,反正后面还是要解释的,就一起得了...
* 到目前为止我们得到的结论是:hashCode & (capacity - 1) ,还没有结束...
*
* 为了更好的说明接下来的问题,我们随便取值一个hashCode来做运算
* 情况A:
* 0000 0010 0100 0110 0000 0110 0000 0011
* &
* 0000 0000 0000 0000 0000 0000 0000 1111
* ---------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 0011
*
* 看情况A我们可以看出对于hashCode来说,它的后面好几位(从右向左看)基本上没啥用,反正不管是1还是0,结果都是0,排面都是靠低位撑着呢...也就是说索引的结果基本上由低位说了算的,所以这样子造成了一个现象,有可能出现高位不同低位相* 同的两个数计算出的索引结果是一致的!看情况B:
*
* 情况B:
* 1111 0000 0000 0000 0000 0000 0000 0011
* &
* 0000 0000 0000 0000 0000 0000 0000 1111
* ----------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 0011
*
* 情况A与情况B的结果是一致的,可是它们的hashCode是不一样的啊,说明了这是两个不同的数,有点问题啊!所以我们必须对这个hashCode在做一次计算,因为要使得高位不同得出的结果应该也要不同,故而要拿这个高位来做文章才能做区分,最简单 * 的方式是拿hashCode的高位与hashCode自身在做一次计算,在HashMap中16位我们称之位高位(从左向右开始数),所以是拿这个16位在做一次运算,同样是采用位运算,有可能是&、|、^(异或)
* 上面我们说,有可能出现高位不同低位相同的两个数,这里也考虑了高位相同低位不同的情况,针对不同的位运算我们采用假设:
*
* 1. 若是采用位运算&,AB两个数的低位都为0,A的高位为1,B的高位为0,结果:A:10 & 01 -> 00 B:00 & 00 -> 00
* 2. 若是采用位运算|,AB两个数的低位都为1,A的高位为0,B的高位为1,结果:A:01 | 00 -> 01 B:11 & 01 -> 11 虽然看着好像并不会造成问题,我们在看看高位相同低位不同的例子:
* 同样采用位运算|,AB两个数的高位都为1,A的低位为0,B的低位为1,结果:A:10 | 01 -> 11 B:11 & 01 -> 11 这里就出现了计算结果一致了
*
* 3. 若是采用位运算^,AB两个数的低位都为1,A的高位为0,B的高位为1,结果:A:01 ^ 00 -> 01 B:11 ^ 01 -> 10
* 同样采用位运算^,AB两个数的低位都为0,A的高位为1,B的高位为0,结果:A:10 ^ 01 -> 11 B:00 ^ 00 -> 00
* 同样采用位运算^,AB两个数的高位都为1,A的低位为1,B的低位为0,结果:A:11 ^ 01 -> 10 B:10 ^ 01 -> 11 (比较高位相同低位不同)
* 同样采用位运算^,AB两个数的高位都为0,A的低位为0,B的低位为1,结果:A:00 ^ 00 -> 00 B:01 ^ 00 -> 01
* 已经很明显了,最终采用的位运算是^,所以会经过如下计算:hashCode ^ (hashCode >>> 16)
*
*
* ==============================================================分割线====================================================================================
*
*
* 上面说的有点多来总结下吧:
* 1. 为什么HashMap的容量大小必须是2的幂次方:因为在HashMap中采用了位运算&代替了取余运算%,这样做是为了提升效率,不过在替换运算符号的同时也有一个要求,为了能取到区间中的每个数,
* 比如0-15中的每个数都能取到,也就是说最后几位要是连续的1(01111,从左向右看),不能是0与1发生间隔,而把这个数去 + 1就等于容量大小,你会发现它正是2的幂次方,所以按照我个人的理解来说,就是在将&替换了%后,为了能取到区间中的每* 个索引,必须是2的幂次方,有人将这种做法叫做均匀分布
*
* 2. 为什么采用hashCode方法:HashMap能存放任意类型的数据,要想按照HashMap的规则进行运算,在任意类型的数据当中一定存在着这些规则,那就只有Object#hashCode
*
* 3. result = hasCode ^ (hashCode >>> 16)目的是什么:为了降低不同的数导致同样的结果(索引),简单来说就是为了降低碰撞,但还是有可能发生碰撞
*
* 4. result & (capacity - 1):计算索引的位置
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量值,必须是2的幂次方
* 当通过构造函数传入更高的值时会使用最大容量指来代替
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的加载因子
* 设置成0.75是对空间与时间的一个权衡(折中),加载因子过大会减少空间开销,增加查找成本
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当添加节点后链表的长度超过该数值时会将链表转换为红黑树,提升查询速度,但同时内存使用会增大,因为树节点的大小约是常规节点的两倍
*
* 为什么是8?
* 在节点良好分布的情况下,基本不会用到红黑树。而在理想情况下的随机哈希,节点分布遵循泊松分布,链表下的长度达到8已经是非常小的概率,超过8的概率我们认为是几乎不可能发生的事情
* 不过HashMap还是做了预防措施,当链表的长度达到8时会被转换成红黑树,至于为什么不是7,个人认为8更合适,应该尽可能的提升性能.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当红黑树的节点个数小于该数值时,红黑树将转换回链表
* 这里有个点很重要,当初我以为红黑树在删除节点后长度就会变小,那应该会按照这个指标来将其变成单向链表结构,可惜不是,红黑树在删除节点前会判断是否此树过小,若过小则转换为链表,若不是则删除节点并进行自我平衡,所以只有在重新散列时* 才会判断该数值!!!!
*
* 为什么不是7?
* 若是频繁地添加删除添加删除元素,那么HashMap将在转换中消耗很大的性能,而7的空隙让它有一个很好的缓冲
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当链表的长度大于8时:
* 若哈希表的容量大于64,则将链表转换成红黑树
* 若哈希表的容量小于64,数据结构保持不变,对哈希表进行扩容,扩容时原来的节点可能在旧的索引上,有可能在新的索引上(原来的索引 + 旧的容量大小)
* 至少应该是4 * TREEIFY_THRESHOLD,防止重新散列和树化阈值(TREEIFY_THRESHOLD)产生冲突,因为如果链表的长度刚好达到8,这个时候转成红黑树,而如果又刚好发生扩容,那么此颗红黑树又将可能被拆分成链表
* 所以一开始的红黑树转化有可能相当于白做了,所以又加上了数组容量为64的限定条件,只能说32比16更适合作为一个限定条件
* 在哈希表容量很小的情况下,随着不断的添加节点,链表的长度会越来越大,也会有越来越多的链表,当长度超过一定的阈值之后便需要转换成红黑树,而在扩容时又需要拆解成链表,这些都是需要一定的成本,所以在容量较小的情况下直接选择扩容
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 自定义加载因子
* 容量大小不变的情况下,加载因子过大减少空间开销,增加查询成本
*
* 注意:该属性是可以指定大于1,但是会造成一定的成本,具体可看threshold属性说明,最终是建议不应该超过1
*/
final float loadFactor;
/**
* 哈希表
* 数组结构中包括链表、红黑树
*/
transient Node<K,V>[] table;
/**
* 缓存entrySet方法的返回值
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 键值对的数量
*/
transient int size;
/**
* HashMap中结构修改的次数
* 在上面的翻译中我们已经解释了什么是结构修改
* 该成员属性是用于检测迭代器的快速失败
*/
transient int modCount;
/**
* 扩容前需要判断的阈值
* 若超过该值则扩容,若没超过则不需要
* 该值的计算方式:capacity * load factor
*
* 注意:该属性是可以超过指定容量大小(capacity),准备来说,应该是加载因子(load factor)可以指定大于1,下面假设是2
* 相当于指定了容量大小是10,但是会到大于20时才会扩充容量
* 当填充的元素个数超过10个而小于20个后,那么后续的元素必定会造成碰撞从而形成链表或红黑树,这就为后续的增加/查找造成了一定的成本
* 所以建议加载因子(load factor)不应该超过1
*
* 我为什么会想到这个注意点呢,是因为putMapEntries方法中有一段代码:float ft = ((float)s / loadFactor) + 1.0F,这段代码很有意思,可以看具体方法中的说明
*/
int threshold;
/**
* 存储Key
*/
transient Set<K> keySet;
/**
* 存储Value
*/
transient Collection<V> values;
}
HashMap的大部分知识点,包括一些细节方面,其实都在上面的注释中提到了,应该尽可能的去理解它!
构造函数
/**
* 指定初始容量与加载因子构造哈希表
* 在上面中我们提到了容量必须是2的幂次方,所以调用tableSizeFor方法来进行调整
* Float.isNaN:检测是否是数字
* @param initialCapacity 指定初始容量
* @param loadFactor 指定加载因子
*/
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);
}
/**
* 指定初始容量与默认加载因子(0.75)构造哈希表
* @param initialCapacity 指定初始容量
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 默认初始容量(16)与默认加载因子(0.75)构造哈希表
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 将指定集合添加到哈希表中,采用默认加载因子
* @param m 指定集合
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
方法说明
简单方法
/**
* 将指定容量大小调整到2的幂次方
* 具体是调整到比该容量还大的最近2的幂次方
* cap = 21 最终调整到 32
* cap = 15 最终调整到 16
* 若是2的幂次方则结果是原来的值
* @param cap 指定容量大小
* @return 2的幂次方
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 将指定集合中的元素添加到哈希表中
* 若哈希表为空,表达式s/loadFactor有可能出现带有小数的情况,比如1.6、1.4这样子的一种格式
* 而后续的 + 1.0F 和 (int)ft 相当于是对小数点做一个向上取整,以尽可能的保证更大容量
* 若哈希表不为空,则预先进行扩容一次,若没有预先进行扩容,而是等到后续添加元素达到阈值后才开始扩容,那个时候随着元素的增加扩容所消耗的时间也会增加,简单来说,减少了一定的时间成本
*
* 总结:
* 若哈希表已初始化,则采用它自身的容量进行扩容
* 若哈希表未初始化,则采用集合的容量作为哈希表的容量大小,前提是大于哈希表的容量大小
* @param m 指定集合
* @param evict
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
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);
}
}
}
/**
* 获取哈希表中的元素个数
* @return 哈希表中的元素个数
*/
public int size() {
return size;
}
/**
* 判断哈希表是否为空
* @return 哈希表是否为空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 根据指定键查找节点
* HashMap在插入节点时先通过hashCode进行比较,若两者相同在通过equals进行比较,若也相同则认为是重复,会进行相应的覆盖,若不相等则用链表或红黑树进行存储
* 按照如此的思路,要查找某个节点,则hashCode与equals必须都相等,若有其中一个不相等则继续查找下一个节点
* @param hash 键的哈希值
* @param key 键
* @return 指定键对应的节点
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
/**
* 根据指定键计算哈希值
* 至于为什么是这么计算的可参考一开始提到的
* @param key 键
* @return 哈希值
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 判断指定类是否实现了Comparable,同时Comparable的泛型是它自身
* 简单来说就是判断指定类C是否满足`class C implements Comparable<C>`这样子的一种格式,若满足则返回该类的class,否则返回null
* @param x 指定类
* @return x.getClass or null
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
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;
}
/**
* 若x与kc的类型相同则比较x与k的大小,实际上k与kc的类类型是同一个,简单来说就是在比较k与x的大小,既然是比较,类类型是一样的才有意义
* @param kc k的类类型
* @param k 比较值
* @param x 另一个比较值
* @return 比较结果
*/
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
/**
* 判断是否包含指定键
* @param key 指定键
* @return 是否包含指定键
*/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
/**
* 初始化哈希表或以两倍的大小进行扩容,扩容后进行重新散列-rehash
*
* rehash机制:
* 若指定索引上存在元素且没有链表或红黑树结构,则在新数组上重新计算索引并填充即可
* 若指定索引上存在元素且结构是链表,将链表分成高低位两条链表并往新数组中填充,高位链表会存储在新索引上(原索引 + 旧容量大小),低位链表存储在原索引上
* 若指定索引上存在元素且结构是红黑树,将红黑树分成高低位两棵树,低位这棵树中的长度若超过8,在判断高位那棵树是否存在,若不存在说明低位已经树形化过,若存在说明结构已经修改,低位需要重新树形化,若低位这棵树的长度不超过8则将这棵树转换* 成单向链表的结构,同理对于处理高位这棵树也是如此判断
* @return 新数组
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧容量
int oldThr = threshold; //旧阈值
int newCap, newThr = 0; //新容量与新阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //旧容量超过最大容量时
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //新容量是旧容量的两倍大小
newThr = oldThr << 1; //新阈值是旧阈值的两倍大小
}
else if (oldThr > 0) //threshold大于0说明采用了自定义的初始容量大小,而一开始threshold存放了初始容量的大小
newCap = oldThr;
else { //threshold小于0说明采用默认初始容量大小与加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { //只有在采用自定义初始容量大小的情况下才会进入到该语句中
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //使用新容量创建新数组以便进行扩容
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) //哈希表指定索引上只有一个元素,也就是说该位置上不存在链表或者红黑树之类的数据结构
newTab[e.hash & (newCap - 1)] = e; //将该位置上的元素按照新容量重新散列,也就是在新数组中重新计算索引并填充,至于为什么是这么计算的,最上面应该提到了
else if (e instanceof TreeNode) //哈希表指定索引上是红黑树,红黑树将自身拆解成高低位两棵树,具体可参见split方法
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
/**
* 哈希表指定索引上是一条链表,根据哈希值将一条链表上的节点分成高位、低位两部分组成的两条链表(分组),将高低位两条链条填充到新数组中,高位填充到新索引(原索引 + 旧容量大小)处,而低位的索引保持不变
*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/**
* 首先应该明白oldCap是2的幂次方,它的二进制应该是0000 0000 0000 0000 0000 0000 0000 1000 这样子的一种格式
* e.hash & oldCap:将结果大于0的节点构成一组链表,既然是链表就有头部节点与尾部节点,所以这就对应了上面的hiHead、hiTail,我们称作高位;同样的将结果小于0
* 的节点构成另外一组链表,对应着loHead、loTail,我们称作低位
*/
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);
/**
* 为什么在新的哈希表中索引只可能出现在旧索引处或旧索引 + 旧容量大小处
* 首先索引的计算方式是:e.hash & (capacity-1),方便理解我们直接举例
* oldCapacity:16 newCapacity:32 node1:0000 0000 0000 0000 0000 1111 0000 1010 node2:0000 0000 0000 0000 0000 1111 0001 1010
* node1 & (oldCapacity -1) node2 & (oldCapacity -1)
*
* 0000 0000 0000 0000 0000 1111 0000 1010 0000 0000 0000 0000 0000 1111 0001 1010
* & 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 0000 0000 0000 0000 0000 0000 1111
* -------------------------------------------- ------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 1010 0000 0000 0000 0000 0000 0000 0000 1010
*
* node1 & (newCapacity -1) node2 & (newCapacity -1)
*
* 0000 0000 0000 0000 0000 1111 0000 1010 0000 0000 0000 0000 0000 1111 0001 1010
* & 0000 0000 0000 0000 0000 0000 0001 1111 & 0000 0000 0000 0000 0000 0000 0001 1111
* -------------------------------------------- ------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 1010 0000 0000 0000 0000 0000 0000 0001 1010
*
* 在旧容量当中索引取决于最后的4位,而在新容量当中索引取决于最后的5位,旧容量与新容量相比最大的差别在于倒数第5位上,而能造成它们索引不同的情况就要看hash值的倒数第5位上是否是1
* 若不是1,&完之后结果自然为0,此时的新容量下的索引与旧容量下的索引是一致的;若是1,&完之后结果自然是1,而正好该位置上是旧容量的大小(倒数第5位),如果转换成十进制的话,此时新容量下的索引是旧容量下的索引 + * 旧容量大小,所以我们可以得出在新容量下的索引只可能出现在这两种情况下。而在计算当中采用 e.hash & oldCap就是在判断倒数第5位是否是1,若是1则索引是旧容量下的索引 + 旧容量大小,若不是1,则还是原来的索引
*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* 将指定索引上的单向链表结构转成红黑树 + 双向链表
* 链表长度不仅要超过8,同时哈希表的长度要超过64,否则直接进行扩容
* 为什么还要采用双向链表呢?
* 因为在转成红黑树后需要将根节点移动到链表的头部,这就需要更改链表节点之间的关系,而单向链表中是不知道上一个节点是谁,若想知道的话就还需要从头遍历,所以采用双向链表
*
* @param tab 新数组
* @param hash 哈希值
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do { //将链表转成双向链表
TreeNode<K,V> p = replacementTreeNode(e, null); //将节点信息变成红黑树节点
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab); //将双向链表转成红黑树
}
}
/**
* 将节点信息转换成红黑树节点信息
* @param p 节点信息
* @param next 下一个节点信息
* @return 红黑树节点
*/
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* 转换成普通的节点信息
* @param p 节点信息
* @param next 下一个节点信息
* @return 普通节点
*/
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
/**
* 链表树形化
* 单向链表 -> 双向链表 + 红黑树
* @param tab 哈希表
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) { //若根节点不存在则进行设置
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) { //存在根节点则要进行左右子树的比较来选择最终的存储点
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) //通过比较hash值,此时说明x节点小于根节点,故应该在左子树
dir = -1;
else if (ph < h) //右子树上
dir = 1;
else if ((kc == null && //若当前节点与另外一个节点的hash相等的话,就比较键值,若键值也相同的话就比较类名
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; //xp是p节点的父节点
if ((p = (dir <= 0) ? p.left : p.right) == null) { //查找到叶子节点后说明后续已经没有节点了,可以进行插入了
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x); //插入后仍然需要维持红黑树的平衡,通过左右旋转的方式来维持平衡
break;
}
}
}
}
moveRootToFront(tab, root); //将根节点移动到链表头部
}
/**
* 先比较两个对象的类名是否相等,若相等则比较两个对象的哈希值大小并返回结果,若不相等则返回结果
* 若对象为空,则哈希值等于0
* @param a 对象
* @param b 另一个对象
* @return 比较结果值
*/
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;
}
/**
* 红黑树中插入节点后维持平衡
* @param root 根节点
* @param x 插入节点
* @return 根节点
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
/**
* xp = x.parent x的父节点
* xpp = x.parent.parent x的爷爷节点
* xppl = x.parent.parent.left x的爷爷节点的左子树
* xppr = x.parent.parent.right x的爷爷节点的右子树
*/
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) { //1 x.parent == null 说明是根节点,将其变成黑色即可
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null) //2 xp = x.parent 说明x的父节点是黑色,不需要任何的操作
return root;
if (xp == (xppl = xpp.left)) { //3 x的父节点在x的爷爷节点的左子树上
if ((xppr = xpp.right) != null && xppr.red) { //4 x的父节点是红色,叔叔节点也是红色
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) { //5 X的父节点在X的爷爷节点的左子树上,X在X的父节点的右子树上
root = rotateLeft(root, x = xp);//注意此时的x变成了原来x的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //6 X的父节点在X的爷爷节点的左子树上,X在X的父节点的左子树上
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else { //7 X的父节点在X的爷爷节点的右子树上
if (xppl != null && xppl.red) { //8 x的父节点是红色,叔叔节点也是红色
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) { //9 x的父节点在X的爷爷节点的右子树上,X在X的父节点的左子树上
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //10 x的父节点在X的爷爷节点的右子树上,X在X的父节点的右子树上
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 红黑树中指定节点进行左旋
* 过多的算法就不讨论了,可以参考红黑树、AVL树的文章
* @param root 根节点
* @param p 指定节点
* @return 根节点
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
/**
* r = p.right 当前节点的右子树
* rl = r.left 当前节点的右子树的左子树
*/
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
/**
* 红黑树中指定节点进行右旋
* 过多的算法就不讨论了,可以参考红黑树、AVL树的文章
* @param root 根节点
* @param p 指定节点
* @return 根节点
*/
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) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
/**
* 将根节点移动到链表的头部
* 看到这里我发现,它不仅保留了双向链表同时增加了红黑树,所以在查找的时候应该是使用的红黑树结构
* @param tab 哈希表
* @param root 根节点
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
/**
* 当前节点作为查找的起始节点,查找其所有子节点中是否有匹配到指定节点,匹配必须是hash相同、equals相等
* @param h 哈希值
* @param k 指定节点
* @param kc 比较器的泛型
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h) //当前节点的hash大于指定节点的hash,后续应该从左子树进行下一轮比较
p = pl;
else if (ph < h) //当前节点的hash小于指定节点的hash,后续应该从右子树进行下一轮比较
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) //当前节点的hash与指定节点的hash相同,同时equals也相等
return p;
//执行到这里说明当前节点的hash与指定节点的hash相同,但是equals不相等,因为接下来不知道该从左子树还是右子树开始查找,所以要先判断是否存在左右子树
else if (pl == null) //若左子树为空,那只能从右子树开始进行下一轮比较
p = pr;
else if (pr == null) // 若右子树为空,那只能从左子树开始进行下一轮比较
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
/**
* 若左右子树都不为空,那就必须定义一种规则了,通过comparable方法来比较key的大小
* 首先得先检测是否实现了comparable才可以进行比较-comparableClassFor
* 接着还得检测是否类型相同,否则也不具备可比性
* 若满足以上两点则返回比较结果
*/
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null) //执行到这里说明无法通过comparable进行比较或者key在比较之后还是相等,只能先尝试从右子树开始查找
return q;
else //若右子树递归后还是未找到,那么就从左子树开始查找
p = pl;
} while (p != null);
return null; //最后未匹配的话就返回null
}
/**
* 将红黑树拆解成高低的两棵树,并根据判断来选择是转换成单向链表还是重新树形化
* 若低位这棵树的长度不超过6,则将其转换成单向链表,同理高位这棵树也是这么判断的
* 若低位这棵树的长度超过6,且另外一棵树高位不存在,说明红黑树并未将其拆解成两棵树,低位这棵树的结构仍是不变,故而不需要进行重新树形化,而若高位这棵树存在,则需要重新树形化,因为结构已经发生变化
* 同理,高位也是如此判断
* @param map 对象
* @param tab 新的哈希表
* @param index 索引
* @param bit 旧的哈希表长度
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) { //尝试将红黑树拆解成高低位的两棵树,至少何为高低位可参考上面的解释
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD) //低位这棵树的长度小于链表的阈值则转换成链表结构
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) //若hiHead为空,说明红黑树并未拆解成高低位两棵树,低位已经被树形化过了不需要重新树形化,若hiHead不为空,说明红黑树已经拆解成高低位两棵树,结构已经发生变化,低位需要重新树形化
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
/**
* 红黑树转换单向链表
* @param map 对象
* @return 链表的头部节点
*/
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;
}
/**
* 在哈希表中通过指定键移除节点
* 返回值是null表示不存在移除的节点
* @param hash 哈希值
* @param key 指定键
* @param value 指定value,通过hash与equals找到节点后还要根据matchValue来判断是否需要判断value值相等
* @param matchValue 标识是否需要判断value是否相等
* @param movable 标识移除节点后是否需要移动其他节点
* @return 移除的节点或null
*/
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //当前节点刚好是要移除的节点,链表的头部或红黑树的根节点
node = p;
else if ((e = p.next) != null) { //走到这里说明头部节点/根节点为匹配上,获取它的下一个节点,若有说明它的结构可能是红黑树或链表,若没有说明不存在移除的节点
if (p instanceof TreeNode) //判断当前节点是否是红黑树结构
node = ((TreeNode<K,V>)p).getTreeNode(hash, key); //从红黑树中查找节点
else {
do { //从链表中查找节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value || //matchValue标识在找到节点后是否需要判断value值是否相等
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) //从红黑树中移除节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) //说明头部节点刚好是要移除的节点,直接将它的下一个节点填充到哈希表中
tab[index] = node.next;
else // 说明链表中找到移除的节点,p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
/**
* 在红黑树中通过指定键查找节点
* 从根节点开始查找
* @param h 哈希值
* @param k 指定键
* @return null或节点
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 红黑树中移除当前节点
* 调用该方法前当前节点必须存在
* 作者说该方法的代码比典型的红黑删除代码更为混乱,因为红黑树在删除中会通过交换值的方式来删除,而在红黑树中还维护了一个双向链表,若是直接通过交换值会对双向链表中节点之间的关系造成错误,因此它采用了另外一种方式-交换树链接
*
* 交换树链接:当前节点与当前节点的右子树的最左节点进行交换,包括parent、left、right节点的关系都发生交换
* @param map 对象
* @param tab 新的哈希表
* @param movable 标识移除节点后是否需要移动其他节点
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
//开始处理链表
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; //first、root:根节点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; //succ:当前节点的下一个节点 pred:当前节点的上一个节点
if (pred == null) //说明当前节点是根节点,直接将它的下一个节点作为根节点
tab[index] = first = succ;
else //说明当前节点是某一个节点,移除当前节点时更改当前节点的上下节点的关系
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null) //走到这里说明当前节点的下一个节点为空,即表明已经没有节点了
return;
if (root.parent != null)
/**
* moveRootToFront是用来维持红黑树的根节点即为链表的头部,但是在该方法在调用moveRootToFront时会进行判断,也就是它可能不会被调用到,那么会造成红黑的根节点与链表的头部节点不一致
* 这里获取的root指向的是链表的头部节点,并不是红黑树的根节点,故而还要往上查找根节点
*/
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
/**
* 通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回,转链表后就无需再进行下面的红黑树处理,这比维持红黑树的平衡来说简单多了
* 因为在删除节点后红黑树需要维持平衡,有可能根节点会发生变化,也有可能被置空(根节点与删除节点之间是父子节点的关系导致),而又由于没有调用moveRootToFront来更新根节点,导致下次在获取根节点时可能获取到的为空
*/
tab[index] = first.untreeify(map); // too small
return;
}
//结束处理链表
//开始处理红黑树
TreeNode<K,V> p = this, pl = left, pr = right, replacement; //p:当前节点 pl:当前节点的左子树 pr:当前节点的右子树
//1. 当前节点有两个子节点的情况下
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) //找到当前节点的右子树的最左节点,即为s
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; //交换s与p的颜色,也就是交换当前节点与最左节点的颜色
TreeNode<K,V> sr = s.right; //最左节点的右子树,即为sr
TreeNode<K,V> pp = p.parent; //当前节点的父节点,即为pp
//第一次调整和第二次调整:将当前节点和最左节点进行了位置调换
//第一次调整
if (s == pr) { //如果当前节点的右节点即为最左节点,则将当前节点的父节点赋值为最左节点,将最左节点的右节点赋值为当前节点
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent; //最左节点的父节点,即为sp
if ((p.parent = sp) != null) { //将当前节点的父节点变成最左节点的父节点
if (s == sp.left)
sp.left = p; //最左节点应该是在左子树上,这边的代码应该是会走进去,而不会走到else中;将最左节点的父节点的左子树变成当前节点
else
sp.right = p;
}
if ((s.right = pr) != null) //最左节点的左子树必定为null,但有可能还有右子树,故而将它的右子树变成当前节点的右子树
pr.parent = s;
}
//第二次调整
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null) //若最左节点的右子树不为空,则赋值replacement为最左节点的右子树
replacement = sr;
else
replacement = p; //若最左节点的右子树为空,则赋值replacement为当前节点
}
//2. 当前节点只有一个子节点且是左子树,replacement赋值当前节点的左子树
else if (pl != null)
replacement = pl;
//3. 当前节点只有一个子节点且是右子树,replacement赋值为当前节点的右子树
else if (pr != null)
replacement = pr;
//4. 当前节点无子节点,即本身是个叶子节点,replacement赋值为当前节点
else
replacement = p;
//第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
if (replacement != p) { //这里说明当前节点不是叶子节点,直接用replacement代替当前节点
TreeNode<K,V> pp = replacement.parent = p.parent; //获取当前节点的父节点
if (pp == null) //说明当前节点是根节点
root = replacement;
else if (p == pp.left) //说明当前节点在父节点的左子树上
pp.left = replacement;
else
pp.right = replacement; //说明当前节点在父节点的右子树上
p.left = p.right = p.parent = null; //当前节点已经被完整的替换为replacement, 将当前节点清空
}
//若当前节点的颜色是红色,可以直接删除,因为删除一个红色并不会影响红黑树的平衡,否则需要进行红黑树的平衡调整,因为删除黑色会导致黑色数目不一致
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { //说明当前节点是叶子节点,也就是说replacement是需要删除的节点,直接将replacement的关系清空即可
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable) //判断是否将红黑树的根节点移动到链表的头部
moveRootToFront(tab, r);
}
/**
* 调整红黑树的平衡
* 该方法可以说是HashMap中最复杂的部分了,不过我认为它是跟算法有些关系,对于源码中的算法个人认为只要你清楚它在做什么即可,毕竟你的算法基本并不怎样,所以这里就不做深入研究了
* @param root 根节点
* @param x 当前节点
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
/**
* x:当前节点
* xp:当前节点的父节点
* xpl:当前节点的父节点的左子树
* xpr:当前节点的父节点的右子树
*/
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root) //当前节点是根节点的情况
return root;
else if ((xp = x.parent) == null) { //当前节点是根节点,染成黑色,直接返回,因为调整过后,root并不一定指向删除操作过后的根节点,如果之前删除的是root节点,则x将成为新的根节点)
x.red = false;
return x;
}
else if (x.red) { //如果x为红色,则无需调整
x.red = false;
return root;
}
else if ((xpl = xp.left) == x) { //当前节点为其父节点的左孩子
if ((xpr = xp.right) != null && xpr.red) { //如果它有红色的兄弟节点xpr,那么它的父亲节点xp一定是黑色节点
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//如果xpr为空,则向上继续调整,将x的父节点xp作为新的x继续循环
if (xpr == null)
x = xp;
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) { //若sl和sr都为黑色或者不存在,即xpr没有红色孩子,则将xpr染红
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
/**
* 清空哈希表
*/
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;
}
}
/**
* 哈希表中是否包含指定值
* @param value 指定值
* @return 是否包含指定值
*/
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) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
/**
* 获取包含所有键的集合
* @return 包含所有键的Set集合
*/
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
/**
* 包含哈希表中所有键的集合
*/
final class KeySet extends AbstractSet<K> {
/**
* 获取哈希表的大小,共存储了多少节点
* @return 哈希表的大小
*/
public final int size() { return size; }
/**
* 清空哈希表
*/
public final void clear() { HashMap.this.clear(); }
/**
* 获取包含所有键的迭代器
* @return 包含所有键的迭代器
*/
public final Iterator<K> iterator() { return new KeyIterator(); }
/**
* 哈希表中是否包含指定键
* @param o 指定键
* @return 是否包含指定键
*/
public final boolean contains(Object o) { return containsKey(o); }
/**
* 根据指定键移除节点
* @param key 指定键
* @return 移除的节点
*/
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
/**
* 获取分割迭代器
*/
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
/**
* 遍历所有键并执行指定动作
* 遍历过程中不允许HashMap调用任何会修改结构的方法,否则最后会抛出异常
* @param action 指定动作
*/
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 获取包含所有值的对象
* @return 包含所有值的对象
*/
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
/**
* 包含所有值的对象
* 和上面的集合类似,就不做介绍了
*/
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 获取包含所有键值对的集合
* @return 包含所有键值对的集合
*/
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
/**
* 包含所有键值对的集合
* 和上面的代码大同小异,不讲解了
*/
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null; //注意这里,倒数第二个参数matchValue为true,说明删除的时候还要比较value值是否相同
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 通过指定键获取节点并替换值
* 该方法会比较旧值是否与获取到的节点的值相同,只有相同的情况下才会替换
* @param key 指定键
* @param oldValue 旧值
* @param newValue 新值
* @return 是否替换成功
*/
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
/**
* 通过指定键获取节点并替换值
* 该方法不会比较值是否相等
* @param key 指定键
* @param value 新值
* @return 旧值或null
*/
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
/**
* 通过指定键获取节点并执行指定动作
* 获取到的节点若不为空同时值不为空,则不会执行指定动作
* 否则执行指定动作获取新值,若新值为空直接返回,若新值不为空则继续
* 若是节点不为空,说明是值为空,则用新值代替
* 若是节点为空,则判断当前位置是否是红黑树结构,若是则采用红黑树的方式新增节点,若不是则采用头插法(将新增的节点插入到链表的头部),同时还要判断链表的长度是否超过8,超过则转换成红黑树
* @param key 指定键值、
* @param mappingFunction 指定动作
* @return 旧值或执行指定动作后的新值
*/
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length; //初始化容量大小或扩容
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
if (old != null && (oldValue = old.value) != null) { //通过键获取到的节点若不为空同时值不为空,则不会执行指定动作
afterNodeAccess(old);
return oldValue;
}
}
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) { //走到这里说明节点的值为空,替换成新值
old.value = v;
afterNodeAccess(old);
return v;
}
else if (t != null) //当前位置的结构是红黑树
t.putTreeVal(this, tab, hash, key, v);
else { //采用头插法
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return v;
}
/**
* 通过指定键获取节点并执行指定动作
* 若是节点为空或节点的值为空,则直接返回
* 若是节点不为空同时值不为空,执行指定动作获取新值,若新值为空则删除当前节点,否则替换当前节点的值
* @param key 指定键值
* @param mappingFunction 指定动作
* @return null或执行指定动作后的新值
*/
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
/**
* 通过指定键获取节点并执行指定动作
* 不管节点是否为空,都会执行指定动作获取新址
* 若节点不为空且新值不为空,则进行替换
* 若节点不为空且新值为空,则移除当前节点
* 若节点为空且新值不为空,则新增节点,当前位置是红黑树结构则采用红黑树的新增方式,否则采用头插法来新增节点
* @param key 指定键值
* @param remappingFunction 指定动作
* @return 执行指定动作后的新值
*/
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode) //在红黑树中查找
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
else if (v != null) {
if (t != null) //当前位置是红黑树结构
t.putTreeVal(this, tab, hash, key, v);
else { //采用头插法
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
/**
* 通过指定键获取节点并执行指定动作
* 若节点不为空且值不为空,则执行指定动作获取新值
* 若节点不为空且值为空,则采用默认值(value)作为新值
* 若新值为空则移除节点,若移除不为空则替换
* 若节点为空且新值不为空,则新增节点,当前位置是红黑树结构则采用红黑树的新增方式,否则采用头插法来新增节点
* @param key 指定键值
* @param value 默认值
* @param remappingFunction 指定动作
* @return 执行指定动作后的新值
*/
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
V v;
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
/**
* 遍历哈希表并执行指定动作
* @param action 指定动作
*/
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
/**
* 遍历哈希表并执行指定动作后获取新值,利用新值替换所有节点的旧值
* @param function 指定动作
*/
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
/**
* 浅拷贝
* @return 克隆后的新对象
*/
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize(); //恢复到初始化
//恢复到初始化后需要重新设置节点,明明一开始已经设置节点了为什么还要恢复初始化后再设置呢?个人认为是因为某些成员属性需要被恢复到初始化,克隆后的对象有可能会被使用到,不能在与克隆前的对象有所关联,初始化便需要重新设置
result.putMapEntries(this, false);
return result;
}
/**
* 获取加载因子
* @return 加载因子
*/
final float loadFactor() { return loadFactor; }
/**
* 获取容量大小
* @return 容量大小
*/
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
/**
* 自定义序列化
* @param s 输出流
*/
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
/**
* 将键值对存储到流中
* @param s 输出流
*/
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
/**
* 自定义反序列化
* @param s 输入流
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // 读取buckets
int mappings = s.readInt(); // 读取size
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f; //向上取整获取初始容量,尽可能的获取到更大的容量以便减少resize的调用
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
/**
* 迭代器基类
*/
abstract class HashIterator {
//下一个节点
Node<K,V> next;
//当前节点
Node<K,V> current;
//记录修改次数
int expectedModCount;
//当前位置
int index;
/**
* 初始化
* 提前准备好第一个节点
*/
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
/**
* 是否存在下一个节点
* @return 是否存在下一个节点
*/
public final boolean hasNext() {
return next != null;
}
/**
* 获取下一个节点
* @return 下一个节点
*/
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
/**
* 移除当前节点
*/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
/**
* 包含所有键的迭代器
*/
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
/**
* 包含所有值的迭代器
*/
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
/**
* 包含所有键值对的迭代器
*/
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
/**
* 创建普通节点
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
/**
* 将红黑树节点转换成普通节点
*/
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
/**
* 创建红黑树节点
*/
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
/**
* 将普通节点节点转换成红黑树节点
*/
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* 恢复到初始状态
*/
void reinitialize() {
table = null;
entrySet = null;
keySet = null;
values = null;
modCount = 0;
threshold = 0;
size = 0;
}
/**
* 访问节点后的动作
*/
void afterNodeAccess(Node<K,V> p) { }
/**
* 插入节点后的动作
*/
void afterNodeInsertion(boolean evict) { }
/**
* 删除节点后的动作
*/
void afterNodeRemoval(Node<K,V> p) { }
/**
* 获取根节点
* @return 根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* 校验红黑树的基本特性
* @return 是否是红黑树
*/
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;
}
获取节点
/**
* 通过键获取对应的值
* @param key 指定键
* @return 键对应的值
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 通过键获取对应的值
* 因为哈希表允许存放null,若获取的值为null则采用默认值
* @param key 指定键
* @param defaultValue 默认值
* @return 键对应的值或默认值
*/
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
添加节点
/**
* 添加指定键值对
* 与putIfAbsent相比该方法的修改一定会生效,不管值是否为空
* @param key 指定键
* @param value 指定值
* @return 旧值或null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 添加指定节点到哈希表中
* 有可能会发生重复添加节点,若发生重复的话会更新指定节点的最新值,若不重复则添加即可,以下的添加方式大同小异,区别在于红黑树可能要做一些调整来维持红黑树的特性
* 若索引上不存在任何节点直接添加即可
* 若索引上已经存在节点且结构是链表,若链表长度不超过8则往链表上添加即可,若超过8则执行树形化操作
* 若索引上已经存在节点且结构是红黑树,调用红黑树的添加节点方法
* 返回值是null表示没有重复添加,返回具体值表示发生重复添加
* @param hash 哈希值
* @param key 指定键
* @param value 指定值
* @param onlyIfAbsent 在键值对存在的情况下发生重复时添加是否不允许修改值,为true则表示不允许
* @param evict 模式
* @return 旧值或null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //初始化容量大小
if ((p = tab[i = (n - 1) & hash]) == null) //指定索引上未存放元素,直接存放即可
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //通过hash与equals来判断是否是重复的节点,若是则先记录下当前节点以便后续替换值
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //以红黑树的方式进行添加节点
else { //不重复的节点但在同一个索引处下有两种可能:hash不同、hash相同但equals不相同
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 当链表长度超过8
treeifyBin(tab, hash); //树形化操作
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 新增节点时存在重复的节点,将该节点更新成最新的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //超过阈值时就需要进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 红黑树中插入节点
* 插入过程中会判断是否重复插入,从根节点开始查找
* 若是返回null表示新增了一个节点,若是返回重复的节点则后续会将这个节点的值修改成最新的
* @param map map对象
* @param tab 哈希表
* @param h 指定节点的哈希值
* @param k 指定节点的键
* @param v 指定节点的值
* @return null或重复的节点
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h) //当前节点的hash值大于指定节点的hash,后续应该从左子树开始查找
dir = -1;
else if (ph < h) //当前节点的hash值小于指定节点的hash,后续应该从右子树开始查找
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) //当前节点的hash值与指定节点的hash值相同、equals也相等,此时的节点就是重复的节点,直接返回即可
return p;
//走到这里说明当前节点的hash值与指定节点的hash值相同,但是equals不相等,而后续不知道该从左子树还是右子树开始查找,故而要指定具体规则,通过comparable来比较当前节点的键与指定节点的键
//但前提是要先检测是否实现了comparable才能比较
//继而还要检测两个键值的类型是否相同,否则也不具备可比性
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
/**
* 走到这里的话说明不具备比较器或者比较之后还是相等或者两个键值的类型不同,那么还是无法决定是要从左子树还是右子树开始查找,所以没办法只能先尝试查抄左子树在查找右子树,个个去尝试下
* searched:标识已经遍历过当前节点的所有子节点,包括子孙节点,为false说明还没有过,那么就递归遍历对比,看是否能够查找到equals相等的节点,如果查找到了,也就是查找到了重复节点,直接返回即可
* 如果查找不到,说明应该新增一个节点
* 在查找过后就修改此值,标识已经遍历过了后续就不用再遍历了
*/
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 不知道查找左子树还是右子树的情况下,只能一个一个去尝试了
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//走到这里说明遍历了整个红黑树都没有找到键值相等的节点,说明该新增一个节点了,那就要思考要新增到左子树上还是右子树上呢?
//而该方法就是决定胜负的那个方法,必须要抉择出是左子树还是右子树
//先比较键的类名是否相等,若相等则比较哈希值并返回结果,若不相等则返回结果,这样子就能决定是新增到左子树还是右子树上
dir = tieBreakOrder(k, pk);
}
//走到这里说明要新增一个节点,而新增一个节点应该先找到叶子节点才能实现最后的插入,找到叶子节点并插入后就是调整节点之间的关系,比较容易理解
//若dir小于或等于0且当前节点的左子树是否为空,若不为空则继续查找,若为空则说明新增的节点可以作为当前节点的左子树
//若dir大于0且当前节点的右子树是否为空,若不为空则继续查找,若为空则说明新增的节点可以作为当前节点的右子树
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//创建一个新的红黑树节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));//新增节点后有可能会导致红黑树失去平衡、根节点的变化,故要做调整以及更新根节点到链表的头部
return null;
}
}
}
/**
* 批量添加节点
* @param m 指定集合
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
/**
* 添加指定键值对
* 与put相比该方法的修改只有在值为空的情况下才会生效
* @param key 指定键
* @param value 指定值
* @return 旧值或null
*/
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
移除节点
/**
* 通过指定键移除节点
* matchVaue = false,故移除过程中不会比较值是否相等
* @param key 指定键
* @return 移除节点的值
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 通过指定键移除节点
* matchVaue = true,故移除过程中还会比较值是否相等
* @param key 指定键
* @return 是否移除成功
*/
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
e.hash & oldCap:其结果不为0个人称之为高位,等于0个人称之为低位。实际上它是在判断新容量下节点的索引位置应该是保持不变还是变成原先的索引(旧容量下的索引位置) + 旧容量大小,至于为什么是这两种可以看上面具体方法的解释。
总结
-
当前位置不存在节点时新增节点:直接添加。 -
当前位置存在节点且是红黑树结构下新增节点:直接添加,添加后可能需要旋转并移动根节点。 -
当前位置存在节点且是链表结构下新增节点:直接添加,添加后检测是否超过红黑树的阈值(8)和当前哈希表的长度是否超过一定容量大小(64),若这两者都满足则先将其单向链表转换成双向链表,遍历双向链表后变成红黑树,此时红黑树中的节点具有prev、next、lef、right、parent信息。 -
当前位置只有一个节点且不存在链表或红黑树结构下进行扩容:在新的哈希表中重新计算位置并填充即可。 -
当前位置有多个节点且是红黑树结构下进行扩容:将红黑树尝试分成高低位两棵树(何为高低位可参考上面的解释),低位这棵树先检测长度是否超过链表的阈值(6),若超过说明要进行树形化,接着检测是否已经树形化过,实际上就是判断高位的那棵树是否为空即可知道低位是否已经树形化过,若是高位那棵树为空,说明红黑树并未分成高低位两棵树,所以最终是将红黑树填充到新的哈希表中;同样的,若是高位那棵树存在,说明红黑树确实分成了两棵树,结构已经发生了变化,低位需要重新树形化;若低位这棵树的长度不超过链表的阈值(6)则要将其转换成单向链表结构并填充到新的哈希表中。高位那棵树的做法和低位是类似的,只不过最终填充到新的哈希表中时索引不一致,低位的索引是原先的索引(保持不变),高位的索引是原来的索引 + 旧的哈希表的容量大小。 -
当前位置有多个节点且是链表结构下进行扩容:将链表尝试分成高低位两条链表并填充到新的哈希表中,低位的索引是原先的索引(保持不变),高位的索引是原来的索引 + 旧的哈希表的容量大小。 -
当前位置存在节点且是红黑树结构下删除节点:查找到指定节点后先调整双向链表,若此时红黑树的长度过小则直接将其转换成单向链表,若不是则利用交换树链接的方式来移除并调整平衡。该内容涉及到比较多的算法,建议对于算法只要懂的它做什么即可,后续想研究算法的话可以在回过头来! -
HashMap的键与值都允许存放Null。
-
HashMap是无序不可重复、非线程安全。
-
HashMap的容量大小必须是2的幂次方。
-
HashMap默认初始容量16、默认加载因子0.75。
-
HashMap中索引的计算方式:hash & (capacity - 1)。
-
HashMap扩容时会以
2倍大小进行增长,旧数组中的节点重新散列到新数组中,而在新数组中索引可能出现在原索引位置或原索引位置 + 旧数组容量大小。 -
多线程下,JDK1.7在扩容时会出现链表节点的互相引用导致了死循环,同时也会造成数据丢失(采用了头插法),而JDK1.8会造成数据丢失(采用了尾插法)。
重点关注
单向链表转成红黑树 红黑树转成单向链表 rehash 默认初始容量与加载因子 hash值与索引的计算方式
浙公网安备 33010602011771号