为什么会有这样的结果呢?
首先,我们来考虑一下常规除法。当我们将一个数除以另一个数时,我们将得到一个商和一个余数。
例如,当我们把 7 除以 3 时,我们得到商 2 和余数 1,因为 (7 = 3 × 2 + 1)。
推荐阅读:
Java 取模和取余
01、取余
:
余数的定义是基于常规除法的,所以它的符号总是与被除数相同。商趋向于 0。
例如,对于
-7 % 3
,余数是
-1
。因为 -7 / 3 可以有两种结果,一种是商 -2 余 -1;一种是商 -3 余 2,对吧?
因为取余的商趋向于 0,-2 比 -3 更接近于 0,所以取余的结果是 -1。
02、取模
:
取模也是基于除法的,只不过它的符号总是与除数相同。商趋向于负无穷。
例如,对于
Math.floorMod(-7, 3)
,结果是
2
。同理,因为 -7 / 3 可以有两种结果,一种是商 -2 余 -1;一种是商 -3 余 2,对吧?
因为取模的商趋向于负无穷,-3 比 -2 更接近于负无穷,所以取模的结果是 2。
需要注意的是,不管是取模还是取余,除数都不能为 0,因为取模和取余都是基于除法运算的。
03、与运算
:
当除数和被除数都是正数的情况下,取模运算和取余运算的结果是一样的。
比如说,7 对 3 取余,和 7 对 3 取模,结果都是 1。因为两者都是基于除法运算的,7 / 3 的商是 2,余数是 1。
于是,我们会在很多地方看到,
取余就是取模,取模就是取余。这是一种不准确的说法,基于操作数都是正数的情况下
。
对于 HashMap 来说,它需要通过
hash % table.length
来确定元素在数组中的位置,这种做法可以在很大程度上让元素均匀的分布在数组中。
比如说,数组长度是 3,hash 是 7,那么 7 % 3 的结果就是 1,也就是此时可以把元素放在下标为 1 的位置。
当 hash 是 8,8 % 3 的结果就是 2,也就是可以把元素放在下标为 2 的位置。
当 hash 是 9,9 % 3 的结果就是 0,也就是可以把元素放在下标为 0 的位置上。
是不是很奇妙,数组的大小为 3,刚好 3 个位置都利用上了。
那为什么 HashMap 在计算下标的时候,并没有直接使用取余运算(或者取模运算),而是直接使用位与运算 & 呢?
因为当数组的长度是 2 的 n 次方时,
hash & (length - 1) = hash % length
。
比如说 9 % 4 = 1,9 的二进制是 1001,4 - 1 = 3,3 的二进制是 0011,9 & 3 = 1001 & 0011 = 0001 = 1。
再比如说 10 % 4 = 2,10 的二进制是 1010,4 - 1 = 3,3 的二进制是 0011,10 & 3 = 1010 & 0011 = 0010 = 2。
当数组的长度不是 2 的 n 次方时,
hash % length
和
hash & (length - 1)
的结果就不一致了。
比如说 7 % 3 = 1,7 的二进制是 0111,3 - 1 = 2,2 的二进制是 0010,7 & 2 = 0111 & 0010 = 0010 = 2。
那为什么呢?
因为从二进制角度来看,hash / length = hash / ${2^n}$ = hash >> n,即把 hash 右移 n 位,此时得到了 hash / ${2^n}$ 的商。
而被移调的部分,则是 hash % ${2^n}$,也就是余数。
${2^n}$ 的二进制形式为 1,后面跟着 n 个 0,那 ${2^n}$ - 1 的二进制则是 n 个 1。例如 8 = ${2^3}$,二进制是 1000,7 = ${2^3}$ - 1,二进制为 0111。
hash % length
的操作是求 hash 除以 ${2^n}$ 的余数。在二进制中,这个操作的结果就是 hash 的二进制表示中最低 n 位的值。
因为在 ${2^n}$ 取模的操作中,高于 ${2^n}$ 表示位的所有数值对结果没有贡献,只有低于这个阈值的部分才决定余数。
比如说 26 的二进制是 11010,要计算 26 % 8,8 是 ${2^3}$,所以我们关注的是 26 的二进制表示中最低 3 位:11010 的最低 3 位是 010。
010 对应于十进制中的 2,26 % 8 的结果是 2。
当执行
hash & (length - 1)
时,实际上是保留 hash 二进制表示的最低 n 位,其他高位都被清零。
& 与运算:两个操作数中位都为 1,结果才为 1,否则结果为 0。
举个例子,hash 为 14,n 为 3,也就是数组长度为 ${2^3}$,也就是 8。
1110 (hash = 14)
& 0111 (length - 1 = 7)
0110 (结果 = 6)
保留 14 的最低 3 位,高位被清零。
从此,两个运算
hash % length
和
hash & (length - 1)
有了完美的闭环。在计算机中,位运算的速度要远高于取余运算,因为计算机本质上就是二进制嘛。
HashMap 的取模运算有两处。
一处是往 HashMap 中 put 的时候(会调用私有的
putVal
方法):
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 数组
HashMap.Node<K,V>[] tab;
// 元素
HashMap.Node<K,V> p;
// n 为数组的长度 i 为下标
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);
}
其中
(n - 1) & hash
为取模运算,为什么没用
%
,我们随后解释。
一处是从 HashMap 中 get 的时候(会调用
getNode
方法):
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 && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第一个节点不是要查找的节点,则遍历节点链表查找
if ((e = first.next) != null) {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
// 如果节点链表中没有找到对应的节点,则返回 null
return null;
}
看到没,取模运算
(n - 1) & hash
再次出现,说简单点,就是把键的哈希码经过
hash()
方法计算后,再和(数组长度-1)做了一个“与”运算。
可能大家在疑惑:
取模运算难道不该用
%
吗?为什么要用位运算
&
呢
?
这是因为
&
运算比
%
更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。
a % b = a & (b-1)
用 ${2^n}$ 替换下 b 就是:
a % ${2^n}$ = a & (${2^n}$-1)
我们来验证一下,假如 a = 14,b = 8,也就是 ${2^3}$,n=3。
14%8(余数为 6)。
14 的二进制为 1110,8 的二进制 1000,8-1 = 7,7 的二进制为 0111,1110&0111=0110,也就是 0
*
${2
0}$+1`*`${2
1}$+1
*
${2
2}$+0`*`${2
3}$=0+2+4+0=6,14%8 刚好也等于 6。
害,计算机就是这么讲道理,没办法,😝
这也正好解释了
为什么 HashMap 的数组长度要取 2 的整次方
。
为什么会这样巧呢?
因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0。
a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0。例如 5&3=1,5 的二进制是 0101,3 的二进制是 0011,5&3=0001=1。
2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了
hash &(length-1)
的最后一位可能为 0,也可能为 1(取决于 hash 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀分布。
换句话说,& 操作的结果就是将哈希值的高位全部归零,只保留低位值。
假设某哈希值的二进制为
10100101 11000100 00100101
,用它来做 & 运算,我们来看一下结果。
我们知道,HashMap 的初始长度为 16,16-1=15,二进制是
00000000 00000000 00001111
(高位用 0 来补齐):
10100101 11000100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101
因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定也是 0,只剩下 4 个低位
0101
,也就是十进制的 5。
这样,哈希值为
10100101 11000100 00100101
的键就会放在数组的第 5 个位置上。
当然了,如果你是新手,上面这些 01 串看不太懂,也没关系。记住 &运算是为了计算数组的下标就可以了。
-
put 的时候计算下标,把键值对放到对应的桶上。
-
get 的时候通过下标,把键值对从对应的桶上取出来。
看下面这个图。
某哈希值为
11111111 11111111 11110000 1110 1010
,将它右移 16 位(h >>> 16),刚好是
00000000 00000000 11111111 11111111
,再进行异或操作(h ^ (h >>> 16)),结果是
11111111 11111111 00001111 00010101
异或(
^
)运算是基于二进制的位运算,采用符号 XOR 或者
^
来表示,运算规则是:如果是同值取 0、异值取 1
由于混合了原来哈希值的高位和低位,所以低位的随机性加大了(掺杂了部分高位的特征,高位的信息也得到了保留)。
结果再与数组长度-1(
00000000 00000000 00000000 00001111
)做取模运算,得到的下标就是
00000000 00000000 00000000 00000101
,也就是 5。
还记得之前我们假设的某哈希值
10100101 11000100 00100101
吗?在没有调用 hash 方法之前,与 15 做取模运算后的结果也是 5,我们不妨来看看调用 hash 之后的取模运算结果是多少。
某哈希值
00000000 10100101 11000100 00100101
(补齐 32 位),将它右移 16 位(h >>> 16),刚好是
00000000 00000000 00000000 10100101
,再进行异或操作(h ^ (h >>> 16)),结果是
00000000 10100101 00111011 10000000
结果再与数组长度-1(
00000000 00000000 00000000 00001111
)做取模运算,得到的下标就是
00000000 00000000 00000000 00000000
,也就是 0。
综上所述,
hash 方法是用来做哈希值优化的
,把哈希值右移 16 位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。
说白了,
hash 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞
。
我这里写了一段测试代码,假如 HashMap 的容量就是第一次扩容时候的 16,我在里面放了五个键值对,来看一下键的 hash 值(经过
hash()
方法计算后的哈希码)和索引(取模运算后)
HashMap<String, String> map = new HashMap<>();
map.put("chenmo", "沉默");
map.put("wanger", "王二");
map.put("chenqingyang", "陈清扬");
map.put("xiaozhuanling", "小转铃");
map.put("fangxiaowan", "方小婉");
// 遍历 HashMap
for (String key : map.keySet()) {
int h, n = 16;
int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
int i = (n - 1) & hash;
// 打印 key 的 hash 值 和 索引 i
System.out.println(key + "的hash值 : " + hash +" 的索引 : " + i);
}
输出结果如下所示:
xiaozhuanling的hash值 : 14597045 的索引 : 5
fangxiaowan的hash值 : -392727066 的索引 : 6
chenmo的hash值 : -1361556696 的索引 : 8
chenqingyang的hash值 : -613818743 的索引 : 9
wanger的hash值 : -795084437 的索引 : 11
也就是说,此时还没有发生哈希冲突,索引值都是比较均匀分布的,5、6、8、9、11,这其中的很大一部分功劳,就来自于 hash 方法。
hash 方法的主要作用是将 key 的 hashCode 值进行处理,得到最终的哈希值。由于 key 的 hashCode 值是不确定的,可能会出现哈希冲突,因此需要将哈希值通过一定的算法映射到 HashMap 的实际存储位置上。
hash 方法的原理是,先获取 key 对象的 hashCode 值,然后将其高位与低位进行异或操作,得到一个新的哈希值。为什么要进行异或操作呢?因为对于 hashCode 的高位和低位,它们的分布是比较均匀的,如果只是简单地将它们加起来或者进行位运算,容易出现哈希冲突,而异或操作可以避免这个问题。
然后将新的哈希值取模(mod),得到一个实际的存储位置。这个取模操作的目的是将哈希值映射到桶(Bucket)的索引上,桶是 HashMap 中的一个数组,每个桶中会存储着一个链表(或者红黑树),装载哈希值相同的键值对(没有相同哈希值的话就只存储一个键值对)。
总的来说,HashMap 的 hash 方法就是将 key 对象的 hashCode 值进行处理,得到最终的哈希值,并通过一定的算法映射到实际的存储位置上。这个过程决定了 HashMap 内部键值对的查找效率。
好,理解了 hash 方法后我们来看第二个问题,HashMap 的扩容机制。
大家都知道,数组一旦初始化后大小就无法改变了,所以就有了
ArrayList
这种“动态数组”,可以自动扩容。
HashMap 的底层用的也是数组。向 HashMap 里不停地添加元素,当数组无法装载更多元素时,就需要对数组进行扩容,以便装入更多的元素;除此之外,容量的提升也会相应地提高查询效率,因为“桶(坑)”更多了嘛,原来需要通过链表存储的(查询的时候需要遍历),扩容后可能就有自己专属的“坑位”了(直接就能查出来)。
来看这个例子,容量我们定位 16:
HashMap<String, String> map = new HashMap<>();
map.put("chenmo", "沉默");
map.put("wanger", "王二");
map.put("chenqingyang", "陈清扬");
map.put("xiaozhuanling", "小转铃");
map.put("fangxiaowan", "方小婉");
map.put("yexin", "叶辛");
map.put("liuting","刘婷");
map.put("yaoxiaojuan","姚小娟");
// 遍历 HashMap
for (String key : map.keySet()) {
int h, n = 16;
int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
int i = (n - 1) & hash;
// 打印 key 的 hash 值 和 索引 i
System.out.println(key + "的hash值 : " + hash +" 的索引 : " + i);
}
来看输出结果:
liuting的hash值 : 183821170 的索引 : 2
xiaozhuanling的hash值 : 14597045 的索引 : 5
fangxiaowan的hash值 : -392727066 的索引 : 6
yaoxiaojuan的hash值 : 1231568918 的索引 : 6
chenmo的hash值 : -1361556696 的索引 : 8
chenqingyang的hash值 : -613818743 的索引 : 9
yexin的hash值 : 114873289 的索引 : 9
wanger的hash值 : -795084437 的索引 : 11
看到没?
-
fangxiaowan(方小婉)和 yaoxiaojuan(姚小娟)的索引都是 6;
-
chenqingyang(陈清扬)和 yexin(叶辛)的索引都是 9
这就意味着,要采用拉链法(后面会讲)将他们放在同一个索引的链表上。查询的时候,就不能直接通过索引的方式直接拿到(
时间复杂度
为 O(1)),而要通过遍历的方式(时间复杂度为 O(n))。
那假如把数组的长度由 16 扩容为 32 呢?
将之前示例中的 n 由 16 改为 32 即可得到如下的答案:
liuting的hash值 : 183821170 的索引 : 18
xiaozhuanling的hash值 : 14597045 的索引 : 21
fangxiaowan的hash值 : -392727066 的索引 : 6
yaoxiaojuan的hash值 : 1231568918 的索引 : 22
chenmo的hash值 : -1361556696 的索引 : 8
chenqingyang的hash值 : -613818743 的索引 : 9
yexin的hash值 : 114873289 的索引 : 9
wanger的hash值 : -795084437 的索引 : 11
可以看到:
-
虽然 chenqingyang(陈清扬)和 yexin(叶辛)的索引仍然是 9。
-
但 fangxiaowan(方小婉)的索引为 6,yaoxiaojuan(姚小娟)的索引由 6 变为 22,各自都有坑了。
当然了,数组是无法自动扩容的,所以如果要扩容的话,就需要新建一个大的数组,然后把之前小的数组的元素复制过去,并且要重新计算哈希值和重新分配桶(重新散列),这个过程也是挺耗时的。
HashMap 的扩容是通过 resize 方法来实现的,JDK 8 中融入了红黑树(链表长度超过 8 的时候,会将链表转化为红黑树来提高查询效率),对于新手来说,可能比较难理解。
为了减轻大家的学习压力,就还使用 JDK 7 的源码,搞清楚了 JDK 7 的,再看 JDK 8 的就会轻松很多。
来看 Java7 的 resize 方法源码,我加了注释:
// newCapacity为新的容量
void resize(int newCapacity) {
// 小数组,临时过度下
Entry[] oldTable = table;
// 扩容前的容量
int oldCapacity = oldTable.length;
// MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30
if (oldCapacity == MAXIMUM_CAPACITY) {
// 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1
threshold = Integer.MAX_VALUE;
return;
// 初始化一个新的数组(大容量)
Entry[] newTable = new Entry[newCapacity];
// 把小数组的元素转移到大数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 引用新的大数组
table = newTable;
// 重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
该方法接收一个新的容量 newCapacity,然后将 HashMap 的容量扩大到 newCapacity。
首先,方法获取当前 HashMap 的旧数组 oldTable 和旧容量 oldCapacity。如果旧容量已经达到 HashMap 支持的最大容量 MAXIMUM_CAPACITY( 2 的 30 次方),就将新的阈值 threshold 调整为 Integer.MAX_VALUE(2 的 31 次方 - 1),这是因为 HashMap 的容量不能超过 MAXIMUM_CAPACITY。
因为 2,147,483,647(Integer.MAX_VALUE) - 1,073,741,824(MAXIMUM_CAPACITY) = 1,073,741,823,刚好相差一倍(HashMap 每次扩容都是之前的一倍)。
接着,方法创建一个新的数组 newTable,并将旧数组 oldTable 中的元素转移到新数组 newTable 中。转移过程是通过调用 transfer 方法来实现的。该方法遍历旧数组中的每个桶,并将每个桶中的键值对重新计算哈希值后,将其插入到新数组对应的桶中。
转移完成后,方法将 HashMap 内部的数组引用 table 指向新数组 newTable,并重新计算阈值 threshold。新的阈值是新容量 newCapacity 乘以负载因子 loadFactor 的结果,但如果计算结果超过了 HashMap 支持的最大容量 MAXIMUM_CAPACITY,则将阈值设置为 MAXIMUM_CAPACITY + 1,这是因为 HashMap 的元素数量不能超过 MAXIMUM_CAPACITY。
那 newCapacity 是如何计算的呢?
int newCapacity = oldCapacity * 2;
if (newCapacity < 0 || newCapacity >= MAXIMUM_CAPACITY) {
newCapacity = MAXIMUM_CAPACITY;
} else if (newCapacity < DEFAULT_INITIAL_CAPACITY) {
newCapacity = DEFAULT_INITIAL_CAPACITY;
}
新容量 newCapacity 被初始化为原容量 oldCapacity 的两倍。然后,如果 newCapacity 超过了 HashMap 的容量限制 MAXIMUM_CAPACITY(2^30),就将 newCapacity 设置为 MAXIMUM_CAPACITY。如果 newCapacity 小于默认初始容量 DEFAULT_INITIAL_CAPACITY(16),就将 newCapacity 设置为 DEFAULT_INITIAL_CAPACITY。这样可以避免新容量太小或太大导致哈希冲突过多或者浪费空间。
Java 8 的时候,newCapacity 的计算方式发生了一些细微的变化。
int newCapacity = oldCapacity << 1;
if (newCapacity >= DEFAULT_INITIAL_CAPACITY && oldCapacity >= DEFAULT_INITIAL_CAPACITY) {
if (newCapacity > MAXIMUM_CAPACITY)
newCapacity = MAXIMUM_CAPACITY;
} else {
if (newCapacity < DEFAULT_INITIAL_CAPACITY)
newCapacity = DEFAULT_INITIAL_CAPACITY;
}
注意,
oldCapacity * 2
变成了
oldCapacity << 1
,出现了左移(
<<
),这里简单介绍一下:
a=39
b = a << 2
十进制 39 用 8 位的二进制来表示,就是 00100111,左移两位后是 10011100(低位用 0 补上),再转成十进制数就是 156。
移位运算通常可以用来代替乘法运算和除法运算。例如,将 0010011(39)左移两位就是 10011100(156),刚好变成了原来的 4 倍。
实际上呢,二进制数左移后会变成原来的 2 倍、4 倍、8 倍,记住这个就好。
接下来,来说 transfer 方法,该方法用来转移,将旧的小数组元素拷贝到新的大数组中。
void transfer(Entry[] newTable, boolean rehash) {
// 新的容量
int newCapacity = newTable.length;
// 遍历小数组
for (Entry<K,V> e : table) {
while(null != e) {
// 拉链法,相同 key 上的不同值
Entry<K,V> next = e.next;
// 是否需要重新计算 hash
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
// 根据大数组的容量,和键的 hash 计算元素在数组中的下标
int i = indexFor(e.hash, newCapacity);
// 同一位置上的新元素被放在链表的头部
e.next = newTable[i];
// 放在新的数组上
newTable[i] = e;
// 链表上的下一个元素
e = next;
}
该方法接受一个新的 Entry 数组 newTable 和一个布尔值 rehash 作为参数,其中 newTable 表示新的哈希表,rehash 表示是否需要重新计算键的哈希值。
在方法中,首先获取新哈希表(数组)的长度 newCapacity,然后遍历旧哈希表中的每个 Entry。对于每个 Entry,使用拉链法将相同 key 值的不同 value 值存储在同一个链表中。如果 rehash 为 true,则需要重新计算键的哈希值,并将新的哈希值存储在 Entry 的 hash 属性中。
接着,根据新哈希表的长度和键的哈希值,计算 Entry 在新数组中的位置 i,然后将该 Entry 添加到新数组的 i 位置上。由于新元素需要被放在链表的头部,因此将新元素的下一个元素设置为当前数组位置上的元素。
最后,遍历完旧哈希表中的所有元素后,转移工作完成,新的哈希表 newTable 已经包含了旧哈希表中的所有元素。
注意,
e.next = newTable[i]
,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素最终会被放到链表的尾部,这就会导致
在旧数组中同一个链表上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上
。
为了解决这个问题,Java 8 做了很大的优化(讲扩容的时候会讲到)。
JDK 8 的扩容源代码:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 获取原来的数组 table
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取数组长度 oldCap
int oldThr = threshold; // 获取阈值 oldThr
int newCap, newThr = 0;
if (oldCap > 0) { // 如果原来的数组 table 不为空
if (oldCap >= MAXIMUM_CAPACITY) { // 超过最大值就不再扩充了,就只好随你碰撞去吧
threshold = Integer.MAX_VALUE;
return oldTab;
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 没超过最大值,就扩充为原来的2倍
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
// 计算新的 resize 上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr; // 将新阈值赋值给成员变量 threshold
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新数组 newTab
table = newTab; // 将新数组 newTab 赋值给成员变量 table
if (oldTab != null) { // 如果旧数组 oldTab 不为空
for (int j = 0; j < oldCap; ++j) { // 遍历旧数组的每个元素
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 如果该元素不为空
oldTab[j] = null; // 将旧数组中该位置的元素置为 null,以便垃圾回收
if (e.next == null) // 如果该元素没有冲突
newTab[e.hash & (newCap - 1)] = e; // 直接将该元素放入新数组
else if (e instanceof TreeNode) // 如果该元素是树节点
((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;
if ((e.hash & oldCap) == 0) { // 如果该元素在低位链表中
if (loTail == null) // 如果低位链表还没有结点
loHead = e; // 将该元素作为低位链表的头结点
loTail.next = e; // 如果低位链表已经有结点,将该元素加入低位链表的尾部
loTail = e; // 更新低位链表的尾结点
else { // 如果该元素在高位链表中
if (hiTail == null) // 如果高位链表还没有结点
hiHead = e; // 将该元素作为高位链表的头结点
hiTail.next = e; // 如果高位链表已经有结点,将该元素加入高位链表的尾部
hiTail = e; // 更新高位链表的尾结点
} while ((e = next) != null); //
if (loTail != null) { // 如果低位链表不为空
loTail.next = null; // 将低位链表的尾结点指向 null,以便垃圾回收
newTab[j] = loHead; // 将低位链表作为新数组对应位置的元素
if (hiTail != null) { // 如果高位链表不为空
hiTail.next = null; // 将高位链表的尾结点指向 null,以便垃圾回收
newTab[j + oldCap] = hiHead; // 将高位链表作为新数组对应位置的元素
return newTab; // 返回新数组
}