一、HashMap属性
先解释下这些属性的概念,有疑问请看思考。
// The default initial capacity - MUST be a power of two.
// 默认初始容量,必须是2的幂次方。即初始容量是2的四次方16【0 0001 左移四位 1 0000】
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// The maximum capacity, used if a higher value is implicitly specified by either of the constructors with arguments.
// MUST be a power of two <= 1<<30.
// 哈希表的最大容量,即1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// The load factor used when none specified in constructor.
// 默认负载因子,这是HashMap里面比较重要的参数,当hash表中的元素数量达到容量的乘以负载因子的时候,哈希表会进行扩容操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// The bin count threshold for using a tree rather than list for a bin. Bins are converted to trees when adding
// an element to a bin with at least this many nodes. The value must be greater than 2 and should be at least 8
// to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage.
// 树化阈值,当一个桶中的元素大于等于8的时候进行红黑树化
static final int TREEIFY_THRESHOLD = 8;
// The bin count threshold for untreeifying a (split) bin during a resize operation. Should be less than
// TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal.
// 扩容期间,红黑树降级为链表的阈值,当一个桶中的元素小于等于6的时候,树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// The smallest table capacity for which bins may be treeified. (Otherwise the table is resized if too many nodes in a bin.)
// Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between resizing and treeification thresholds.
// 当桶的个数达到64的时候,才进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
// The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two.
// (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)
// 哈希表,HashMap 在构造函数中不会初始化 table 数组,而是在第一次调用 put 方法添加元素时才进行初始化。
transient Node<K,V>[] table;
// Holds cached entrySet(). Note that AbstractMap fields are used for keySet() and values().
// 在 HashMap 里,entrySet() 方法返回的是一个包含所有键值对(Map.Entry 对象)的集合。
transient Set<Map.Entry<K,V>> entrySet;
// The number of key-value mappings contained in this map.
// 元素数量
transient int size;
// The number of times this HashMap has been structurally modified Structural modifications are those that change the number
// of mappings in the HashMap or otherwise modify its internal structure (e.g., rehash). This field is used to make iterators
// on Collection-views of the HashMap fail-fast. (See ConcurrentModificationException).
// 记录其结构修改(改变映射数量也就是元素数量或内部结构如重新哈希)次数的字段,该字段用于让 HashMap 集合视图的迭代器具备快速失败机制,
// 以避免并发操作导致的迭代问题,若迭代中结构被修改则抛出 ConcurrentModificationException 异常。
transient int modCount;
// The next size value at which to resize (capacity * load factor).
// The javadoc description is true upon serialization.Additionally, if the table array has not been allocated, this field holds
// the initial array capacity, or zero signifying DEFAULT_INITIAL_CAPACITY.
// 扩容的阈值(容量 * 负载因子),但是非常需要注意的是当hash数组尚未分配,这个字段保存的是初始数组容量,或者0,或者是默认初始容量
int threshold;
// The load factor for the hash table.
// 负载因子,可以通过构造函数赋值,如果没有指定,那么使用DEFAULT_LOAD_FACTOR
final float loadFactor;
思考
-
为什么默认初始容量需要是2的幂次方?
因为HashMap
采用哈希表存储键值对,2的幂次方容量有助于提升哈希计算与冲突处理效率。 -
为什么使用位移计算?
位运算比乘法运算效率更高,能加快数据处理速度。 -
“元素数量”和“容量”有何区别?
容量:指HashMap
中桶(bucket)的数量,用于分配键值对存储位置,单个桶可容纳多个键值对,并可能触发红黑树化或链表降级。
元素数量:指实际存储的键值对总数,由Map.Entry
对象表示。 -
非扩容期间,
UNTREEIFY_THRESHOLD
是否起作用?
目前源码中该阈值在删除元素时可能触发红黑树降级为链表,具体逻辑待进一步分析。 -
MIN_TREEIFY_CAPACITY
和TREEIFY_THRESHOLD
如何联动?
向HashMap
添加元素时,若桶内节点数达到TREEIFY_THRESHOLD
,会检查容量:若小于MIN_TREEIFY_CAPACITY
,则扩容缓解冲突;若大于等于MIN_TREEIFY_CAPACITY
,则将桶内链表转换为红黑树。 -
Node<K,V>[] table
和entrySet()
存储元素的区别?
存储形式:table
是基于哈希值索引的数组,通过链表或红黑树处理冲突;entrySet()
返回的集合抽象展示键值对,不涉及底层存储细节。
用途:table
用于HashMap
内部的插入、查找、删除操作;entrySet()
便于外部遍历和数据处理。 -
threshold
为何存在两种类型的值?
在HashMap
未初始化时,threshold
存储初始容量(必为2的幂次方):若构造函数指定容量为8,threshold
即为8;若指定9,则取最近的2的幂次方值16作为容量。
二、HashMap的构造函数
无参构造函数
loadFactor
:赋值DEFAULT_LOAD_FACTOR
initcapacity
:0
threshold
:0
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
有参构造函数 - initialCapacity
loadFactor
loadFactor
:通过入参指定
initcapacity
:通过入参指定
threshold
:通过initcapacity
计算得到threshold
(返回大于等于指定容量的的最小的2的n次幂),这个threshold
用于创建table
的时候初始化容量
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 指定容量只能大于等于0,否则抛异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) // 指定容量超过了最大容量,取最大容量计算threshold
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 负载因子小于等于0或者非float,抛异常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; // 负载因子赋值为入参指定
this.threshold = tableSizeFor(initialCapacity); //返回得到>=cap的最近的2的幂次方
}
想要看懂下面的代码,那么需要了解下原码、反码、补码的概念,如果懂的话,可以跳过这段。
原码:最高位为符号位:0
表示正数、1
表示负数;其余位:数值的绝对值
十进制+5
的8
位原码:0000 0101
十进制-5
的8
位原码:1000 0101
反码:正数的反码与原码相同,负数的反码取值逻辑为符号位不变,其余各位取反
十进制+5
的8
位反码:0000 0101
十进制-5
的8
位反码:1111 1010
补码:正数的补码和原码相同,负数的补码在反码基础上+1
十进制+5
的8
位补码:0000 0101
十进制-5
的8
位补码:1111 1011
计算机内部都是使用补码的,是因其能统一处理符号位和数值位,简化加减法运算(将减法变加法)就比如+5
和-5
,其补码相加刚好为0000 0000
,解决零表示不唯一问题,契合硬件特性,且可扩大负数表示范围。
其他扩展:
按位与&
:对应位都为1
,结果才为1
,否则为0
按位或|
:对应位有1
,结果才为1
,否则为0
按位非~
:对应位为0
,结果为1
,否则为0
按位异或^
:对应位不同,结果为1
,否则为0
按位左移<<
:整个位数向左移动n
位,右边补0
,相当于乘2
按位右移>>
:整个位数向右移动n
位,左边根据符号位补,正数补0
,负数补1
,相当于除以2
。
无符号按位右移>>>
:整个位数向右移动n
位,正数和负数的左边都用0
来补位
1110
减1
:低位向高位借1
,借位后个位变为2
,结果位1101
1100
减1
:低位向高位借1
,高位是0
,继续向高位的高位借1
,结果为1011
// 这个方法是计算一个 32 位整数 i 二进制表示中前导零的数量。前导零指的是从二进制数的最高位开始连续的 0 的个数。类似于二分查找算法
// 例如,二进制数 0000 0000 0000 0000 0000 0000 0000 1010 有 31 个前导零。
public static int numberOfLeadingZeros(int i) {
// 如果i<0,代表是赋值,最高位符号位就是1,那么前导零为0个,如果i=0,二进制都为0,所以前导零为32个
if (i <= 0)
return i == 0 ? 32 : 0;
// 如果i>0,那么前导零最多31,先将n设置为31
int n = 31;
// 1<<16也就是0000 0000 0000 0001 0000 0000 0000 0000,如果i比1<<16还大,那么说明前导零最多15个,
// 也就是n = 31 - 16 = 15, 继续将i无符号右移 16 位,将高 16 位移到低 16 位,方便后续检查
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
// 同理 1<<8也就是0000 0001 0000 0000,如果此时的i比1<<8大,那么说明前导零最多7个,
// 也就是n = 15 - 8 = 7,继续将i无符号右移 8 位,将高 8 位移到低 8 位,方便后续检查
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
// 同理 1<<4也就是0001 0000,如果此时的i比1<<4大,那么说明前导零最多3个,
// 也就是n = 7 - 4 = 3,继续将i无符号右移 4 位,将高 4 位移到低 4 位,方便后续检查
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
// 同理 1<<2也就是0100,如果此时的i比1<<2大,那么说明前导零最多1个,
// 也就是n = 3 - 2 = 1,继续将i无符号右移 2 位,将高 2 位移到低 2 位,方便后续检查
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
// 无符号右移1位,检查此时的最高位是0还是1,结果为0,i的前导零的个数即为n,如果是1,那么则为n-1
return n - (i >>> 1);
}
// int类型是32位,所以在计算机里面是使用32位的二进制存储的,负数使用反码存储的。
static final int tableSizeFor(int cap) {
// 为什么使用cap - 1,这是为了处理 cap 本身就是 2 的幂次方的情况。
// 如果 cap 已经是 2 的幂次方,减 1 后可以确保后续计算得到的结果仍然是 cap。
// -1在计算机中的存储为1111 1111 1111 1111 1111 1111 1111 1111(补码),
// 假设计算出来的前导数为29,以 cap = 8 为例,cap - 1 = 7,
// 二进制表示为 0000 0000 0000 0000 0000 0000 0000 0111,其前导零数量为 29。
// 将 -1 无符号右移 29 位后得到的二进制数是 0000 0000 0000 0000 0000 0000 0000 0111
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
// //根据 n 的值进行边界检查和调整,
// 加 1 后变为 0000 0000 0000 0000 0000 0000 0000 1000,即十进制的 8,这正是我们期望的结果。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
有参构造函数 - initialCapacity
loadFactor
:赋值DEFAULT_LOAD_FACTOR
initcapacity
:通过入参指定
threshold
:通过initcapacity
计算得到threshold
(返回大于等于指定容量的的最小的2的n次幂),这个threshold
用于创建table
的时候初始化容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
有参构造函数 - map
loadFactor
:赋值DEFAULT_LOAD_FACTOR
initcapacity
:先通过float ft = ((float)s / loadFactor) + 1.0F
计算预分配的哈希表容量,再以int t = ((ft < (float)MAXIMUM_CAPACITY)? (int)ft : MAXIMUM_CAPACITY)
确定最终预分配容量得到initcapacity
。 后续会详细分析putMapEntries
方法的。
threshold
:最后用threshold = tableSizeFor(t)
更新阈值 。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
三、从putMapEntries开始
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) {
//计算预分配的hash表的容量
float ft = ((float)s / loadFactor) + 1.0F;
//确定最终的预分配容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
// 如果预分配的容量还小于初始容量(注意这里说的不是阈值,而是指初始容量,因为此时table为空)
if (t > threshold)
//通过tableSizeFor方法计算得到threshold(>=t的最小的2的n次幂)
threshold = tableSizeFor(t);
} else {
//如果hashtable已经被创建了,那么我们需要考虑是否扩容,
// 扩容的条件是,元素数量超过了阈值,并且hash表的长度小于最大容量,如果都满足,那么我们需要进行扩容操作。
while (s > threshold && table.length < MAXIMUM_CAPACITY)
//进行扩容,后面会具体讲resize()的。
resize();
}
// 开始遍历,讲旧表中的元素一个一个重新放入到新表中,请注意hash值是不变的,只是重新position到新的表中。
// 简单来说就是新的索引位置i = (n - 1) & hash。为什么呢?n是table的size,也就是2的幂次方,
// n-1的二进制肯定全为1,比如说n = 8,也就是2的三次方,二进制表示1000,n-1 = 7, 其二进制表示0111,
// 使用(n - 1) & hash)就可以保证元素一定会hash到数组中(数组的索引是0 到 n-1,把 hash 值的高位部分屏蔽掉,只保留低位部分,
// 从而确保计算出的索引值在 0 到 n - 1 的范围之内,也就是哈希表数组的有效索引范围)。
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);
}
}
}
四、红黑树
按照二叉查找(排序)树BST -> 平衡二叉树AVL -> RBT红黑树的顺序简单介绍如下:
二叉搜索树(BST)
二叉搜索树(Binary Search Tree),也称为二叉排序树,具有以下特性:
- 特性1:左子树上所有结点的值均小于或等于它的根结点的值。
- 特性2:右子树上所有结点的值均大于或等于它的根结点的值。
- 特性3:左、右子树也分别为二叉排序树。
完美的二叉搜索树示例如下:

然而,当二叉搜索树退化成链表时,会呈现为如下形态:

此时可以看到二叉搜索树的缺点,其最坏情况下的搜索复杂度为 O ( n ) O(n) O(n)。为了避免这种特殊情况的发生,引入了平衡二叉树(AVL)和红黑树(red-black tree)。
平衡二叉树(AVL)
平衡二叉树(AVL树)由两位俄罗斯数学家G.M. Adelson-Velsky和E.M. Landis提出,具有以下特性:
- 特性1:对于任何一颗子树的root根结点而言,它的左子树任何节点的key一定比root小,而右子树任何节点的key 一定比root大。
- 特性2:对于AVL树而言,其中任何子树仍然是AVL树。
- 特性3:每个节点的左右子节点的高度之差的绝对值最多为1。
对这些特性的理解是:特性1表明AVL树继承于BST,而特性3是AVL树保持平衡的条件。一旦插入、删除操作破坏了平衡,AVL树会自动调整以保持平衡。
那么是如何保持平衡的呢?继续往下看
以下是AVL树四种失衡场景及自平衡的图文说明:




红黑树具有以下特性:
- 每个节点的颜色,非红即黑。
- 根节点的颜色必定为黑色。
- 每个叶子节点(NIL节点,即空节点)均为黑色。
- 若一个节点为红色,那么它的两个子节点都为黑色。
- 从任意一个节点出发,到该节点的所有后代叶子节点的所有路径上,所包含的黑色节点数目相同。

需重点关注:
NIL 节点特性:红黑树的叶子节点 NIL 为虚拟节点。当插入红色新节点时,该节点实际附带两个黑色 NIL 空节点,这是维持树结构规则的重要机制。
设计原理:红黑树采用空间换时间策略,通过为节点增设颜色属性,减少平衡操作次数,提升动态操作效率。
高度本质:若移除所有红色节点,会发现从根节点到任意叶子节点的路径高度一致。因此,红黑树的有效高度由黑色节点数量决定。

红黑树插入节点的规则:
默认新插入的节点为红色,这是因为父节点为黑色的概率较大,新节点为红色可以避免颜色冲突。具体插入情况如下:
- 红黑树为空树:直接插入新节点,并将其颜色设为黑色。
- 插入节点的key已存在:直接更新节点值,节点颜色不变。
- 插入节点的父节点为黑色:直接插入新节点,不会影响树的平衡。
- 插入节点的父节点为红色且父节点的兄弟节点为红色
- 变色处理:将(祖父节点、父节点、当前节点)的颜色组合由“黑红红”变为“红黑红”,同时将父亲和兄弟节点的颜色由红色变为黑色。
- 自平衡处理:如果祖父节点的父节点是黑色,则无需再处理;否则将祖父节点设置为当前节点,继续插入操作并进行自平衡处理,直至树达到平衡状态。若祖父节点就是根节点,那么直接将祖父节点变为黑色即可。
- 插入节点的父节点为红色、父节点的兄弟节点为黑色(NIL节点)且父亲节点是祖父节点的左子节点(会存在LL和LR失衡问题)
- 5.1 LL失衡问题(新插入节点为父节点的左子节点)
- 变色处理:将颜色组合由“黑红红”变为“红黑红”。
- 旋转操作:对祖父节点进行右旋操作(变色后,父亲节点所在路径会多一个黑色节点,破坏了平衡)。
- 5.2 LR失衡问题(新插入节点为父节点的右子节点)
- 旋转操作:对父节点进行左旋操作,使原来的父节点变为当前节点的左子节点。
- 自平衡处理:与LL失衡情况相同,将新的左子节点设为当前节点,继续进行插入操作。
- 5.1 LL失衡问题(新插入节点为父节点的左子节点)
- 插入节点的父节点为红色、父节点的兄弟节点为黑色(NIL节点)且父亲节点是祖父节点的右子节点(会存在RL和RR失衡问题)
- 6.1 RR失衡问题(新插入节点为父节点的右子节点)
- 变色处理:将颜色组合由“黑红红”变为“红黑红”。
- 旋转操作:对祖父节点进行左旋操作(变色后,父亲节点所在路径会多一个黑色节点,破坏了平衡)。
- 6.2 RL型失衡问题(新插入节点为父节点的左子节点)
- 旋转操作:对父节点进行右旋操作,使原来的父节点变为当前节点的右子节点。
- 自平衡处理:与RR失衡情况相同,将新的右子节点设为当前节点,继续进行插入操作。
- 6.1 RR失衡问题(新插入节点为父节点的右子节点)
五、深入resize()
// 初始化表的大小或者使表的大小增倍,如果为null,则根据字段阈值中持有的初始容量目标进行分配。否则因为我们使用的是2的幂扩展,
// 所以每个bin中的元素必须保持相同的索引或者在新表中以2的幂偏移量移动。
final Node<K,V>[] resize() {
// 赋值oldTab为当前表
Node<K,V>[] oldTab = table;
// 赋值oldCap为当前表的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 赋值oldThr为当前阈值
int oldThr = threshold;
// 初始化newCap和newThr为0
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果旧表容量大于0且容量大于等于最大容量,直接将阈值调成最高阈值,返回旧表,不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果旧表容量大于0且新表容量(=旧表容量扩一倍)后小于最大容量且旧表容量大于等于默认初始容量,则将阈值扩大一倍
// 为什么要oldCap >= DEFAULT_INITIAL_CAPACITY这个条件呢?
// 当 oldCap >= DEFAULT_INITIAL_CAPACITY 时,说明哈希表已经有一定规模,此时进行翻倍扩容可以有效减少哈希冲突,提高性能。
// 当 oldCap < DEFAULT_INITIAL_CAPACITY 时,使用默认初始容量和阈值可以避免前期过度占用内存,同时简化初始化过程。
// 这种策略在保证性能的前提下,尽可能地平衡了内存使用和计算开销。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 如果oldCap<=0,说明hash table还未创建,但是oldThr>0说明oldThr存储的是初始容量,新容量就等于这个初始容量
else if (oldThr > 0)
newCap = oldThr;
// 如果oldCap<=0,oldThr <=0 那么说明表也没创建,初始容量也没有指定,那么直接用默认的初始容量和默认的负载因子*默认初始容量作为阈值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值还为0,(也就是oldCap>0的情况下某些条件不满足 || oldThr > 0) 那么基于新表容量和负载因子计算预分配的阈值。
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 如果新表容量小于最大容量且阈值小于最大容量,那么直接用预分配的阈值作为新阈值,否则用Integer的最大值2^31 - 1作为新阈值
// 为什么不用newCap * loadFactor 呢?
// 是为防止 newCap * loadFactor 结果超出 MAXIMUM_CAPACITY 导致的溢出问题,同时可表示哈希表已达最大容量的特殊状态
// 并简化边界条件处理,提升代码稳定性与性能。
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)
// 旧表中e的位置是通过(n - 1)&hash)计算索引保证索引范围处于哈希表数组的有效索引范围内,n表示数组容量,这里同样也是。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果是红黑树,那么需要将红黑树的每个节点也重新position,是根据next来的,每个节点都会存入他的下一个节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 如果是链表,那么也是将旧表中的元素重新散列到新表中。使用头插法,jdk1.8之前的尾插法会存在死循环问题。
// 为什么存在死循环问题呢?现存在链表A->B->C->D,假设存在线程1和线程2,线程1拿到A,下一个指向节点是B 此时暂停
// 线程2同样拿到A,下一个节点指向B,将B插入到A的头部以后,再拿到C,最终完成插入,得到结果如下: D -> C -> B -> A
// 此时线程2恢复工作,插入B以后,拿到B的下一个节点,发现又是A,那么就变成了B->A->.....->B->A
// 不过根本原因还是HashMap不能在多线程环境下使用!!!
// loHead 表示低位链表的头部节点 loHead 表示低位链表的尾部节点
Node<K,V> loHead = null, loTail = null;
// hiHead 表示低位链表的头部节点 hiTail表示低位链表的尾部节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 为什么要用e.hash & oldCap == 0进行判断呢?
// 假设oldCap的十进制为8,即2的3次方,二进制位为0000 1000,而oldCap-1的二进制位0000 0111
// 假设e的hash为1101 1111 ,那么结果为0000 1000
// 假设e的hash为1101 0111 ,那么结果为0000 0000
// 旧表中e的索引位置通过hash以后分布在0~111之间,
// 所以当e.hash & oldCap == 0也就意味着这个e还在低位,那么新索引在新表中还保持相同的索引,
// 如果e.hash & oldCap == 1,那么意味着这个e会被重新散列到高位,那么新索引在新表中以2的幂偏移量即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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;//新索引在新表中还保持相同的索引
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;//新索引在新表中以2的幂偏移量即oldCap移动
}
}
}
}
}
return newTab;
}
// 在 HashMap 扩容时对红黑树进行拆分操作。当 HashMap 扩容时,原有的红黑树节点需要重新分配到新的数组桶中,
// 该方法会根据节点的哈希值和扩容时的偏移量(bit)将红黑树拆分成两个链表或红黑树,分别放置在新数组的不同位置。
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// loHead 表示低位链表的头部节点 loHead 表示低位链表的尾部节点
TreeNode<K,V> loHead = null, loTail = null;
// hiHead 表示高位链表的头部节点 hiHead 表示高位链表的尾部节点
TreeNode<K,V> hiHead = null, hiTail = null;
// lc 低位链表的节点数量:用于判断是否需要树化treeify还是非树化untreeify
// hc 高位链表的节点数量:用于判断是否需要树化treeify还是非树化untreeify
int lc = 0, hc = 0;
// 遍历当前红黑树的所有节点
for (TreeNode<K,V> e = b, next; e != null; e = next) {
// 保存下一个节点
next = (TreeNode<K,V>)e.next;
// 断开当前节点的 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) {
// 如果低位链表的节点数量小于等于红黑树降级为链表的阈值6,将红黑树转换为普通链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null)
loHead.treeify(tab);
}
}
if (hiHead != null) {
// 如果高位链表的节点数量小于等于红黑树降级为链表的阈值6,将红黑树转换为普通链表
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
以上内容为自己阅读源码+结合网上经验所得,如有不对之处,望指出!
我会持续更新的,目前还只看了部分源码
时间:2025/05/05
JDK版本:17