前面已经看过JDK1.7的HashMap,下面来翻一下JDK1.8的HashMap
HashMap
JDK1.8的HashMap与JDK1.7的一样,都是继承了AbstractMap,然后实现了Map接口,前面已经提过,这一步是比较冗余的,因为AbstractMap已经实现了Map接口,所以这里没必要再去实现一次Map接口
变量和常量
相比于JDK1.7,JDK1.8少了一点东西,又多了一点东西
下面是JDK1.7的变量和常量
JDK1.7与JDK1.8的比较
JDK1.8的删除
- 删除了ALTERNATIVE_HASHING_THRESHOLD_DEFAULT,这个常量是当使用字符串来代替hashcode的时候,HashMap默认的阈值
- 删除了EMPTY_TABLE,这个常量是未进行扩容使用时的底层主数组(第一次添加会导致第一次扩容为16)
- 删除了hashseed,这个变量是使用字符串来代替hashcode时候需要使用的
JDK1.8的变动
- 底层的主数组不是一个Entry数组,而变成一个Node数组了,也就是底层数组发生了变化
下面来看看这两个对象有什么不同
JDK1.8的Node
JDK1.7的Entry
可以看到,区别仅仅在于对hash属性的不同,1.8的Node对象的hash属性是final的,而1.7的Entry对象的hash属性并不是final的,是可以变的
同时,我们看看JDK1.8的TreeNode
可以看到,其继承了LinkedHashMap的Entry,而且LinkedHashMap的Entry是有链表性质的,所以TreeNode也会有链表性质(下面树化是要用到树结点的链表性质的)
JDK1.8的新增
- 新增了静态常量TREEIFY_THRESHOLD
- 新增了静态常量UNTREEIFY_THRESHOLD
- 新增了静态常量MIN_TREEIFY_CAPACITY
新增的TREEIFY_THESHOLD变量如下
我们再来看看他的注释
可以得知这个变量其实就是一个计数阈值,当发生哈希冲突时,形成的链表元素数量若超过这个计数阈值就会变成红黑树
新增的TREEIFY_THRESHOLD常量如下
看看注释
可以知道,这个也是一个计数阈值,只不过跟前面的TREEIFY_THRESHOLD不同,这个是从红黑树变回链表的计数阈值,当红黑树的元素数量低于等于6时,就要变回链表
新增的MIN_TREEIFY_CAPACITY如下
看下注释
可以知道,这个也是一个计数阈值,底层主数组,即Node数组元素数量必须要大于等于这个这个阈值,才可以进行树化操作,也就是从链表变成红黑树,而且注释还提到,这个值最少要为4倍的TREEIFY_THRESHOLD(这个值为8),才不会跟树化阈值(TREEIFY_THRESHOLD)产生冲突。
接下来再来讲讲其他变量和常量的作用吧
Default_initial_capacity
默认的底层主数组长度,默认为16,而且要必须为2的幂次方
MaxIMum_Capacity
底层主数组长度最大值,为
2
30
2^{30}
230
Default_Load_Factor
默认的负载因子,默认值为0.75,当HashMap的元素数量超过底层数组的长度的75%,就要进行扩容(不发生哈希冲突)
size、modCount、threshold
size是记录HashMap存储的元素数量。
modCount相当于是一个底层数组版本号,当发生一次新增、扩容和删除元素这个modCount都会自增,代表底层数组发生变化
threshold是阈值,当底层数组元素数量超过这个阈值就要发生扩容,这个阈值通常为,底层数组长度乘上负载因子
loadFactory
这个是负载因子,虽然前面也有负载因子,只不过前面的负载因子是默认的,而实际操作和判断都是根据这个loadFactor去进行的
构造函数
构造函数这里只讲三个主要的
比较JDK1.7,不同之处有几点
- 无参构造不再是直接使用this加默认值,去调用完整的有参构造方法,而是直接让负载因子为默认值0.75
- 第二个有参构造没有变,还是使用this去调用完整的有参构造方法
- 完整的有参构造方法前面一系列的判断方法没变,不过最后初始化threshold是使用tableSizeFor方法
接下来,我们来看看tableSizeFor方法干了什么
可以看到这个方法是得到指定值最近的大二次幂(比指定值大的二次幂)
前面学习JDK1.7的时候,也是使用threshold来暂存capacity的,但capacity是要规定为二的幂次方的,所以jdk1.8在构造方法这里进行了处理,求出capacity的最近的大二次幂,而JDK1.7是在调用add的时候才进行处理的,其实没什么区别
总结一下完整构造参数的步骤
- 判断指定的capacity是否为0,为0就抛出异常
- 判断指定的capacity有没有超过允许的最大值MaxIMum_Capacity( 2 30 2^{30} 230),如果超过,设为允许的最大值
- 判断负载因子是否为0,或是否为Nan(不合法的浮点数),如果是就抛出异常
- 负载因子初始化
- 阈值初始化,初始化为capacity的最近大二次幂
put方法
接下来我们看新增元素的put方法
这个方法调用了putVal方法,参数为key的hash值,key值和value值,还有两个false
先看一下hash方法
hash
步骤如下
- 先判断key是否为null
- 如果为null,那么hash值就为0,返回0
- 如果不为null,调用key的hashcode方法并且异或上key的hashcode的高16位(hashcode为Integer类型,也就是4个字节,对应32为,向右无符号移动16位,那么就相当于仅仅对高16位进行异或),所以总的结果,就是哈希值的高16位与0、低16位与高16位进行异或结果
那么我们来看一下jdk1.7的
太复杂了,我还是看不懂。。。。
putValue
接下来看看真正添加元素的细节
源码如下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//针对第一次添加元素,因为第一次初始化的时候,底层数组并没有初始化
//判断底层数组是否为null,或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
//如果满足第一次添加元素情况
//要进行第一次扩容
//记录扩容后数组的长度
n = (tab = resize()).length;
//计算哈希值对应的索引位置
//算法依然为哈希值与上最大索引
//注意,这里第一次if判断就对n进行了初始化
//判断底层数组这个索引位置有没有节点
if ((p = tab[i = (n - 1) & hash]) == null)
//如果没有,就直接添加进去
tab[i] = newNode(hash, key, value, null);
//如果有,代表发生了哈希冲突,需要进行处理
else {
//
Node<K,V> e;
K k;
//注意,上面的if就给p初始化了
//这里是判断是否是同一个key,如果是同一个Key,就要替换value
//先判断哈希值是否相等
//再判断key是否相等(判断是否是同一个对象,如果不是,使用equals)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//判断key是相等的,进行替换值
//这里是先记录相等Key的键值对
e = p;
//虽然key不相等,
//但可能是与后面的红黑树或者链表里面的键值对发生冲突
//判断是不是红黑树(判断第一个节点是不是TreeNode)
else if (p instanceof TreeNode)
//如果是红黑树,那么就调用putTreeVal方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是红黑树,那就肯定是链表
else {
//因为是链表,所以要判断是否要进行树化
//使用for循环遍历整个链表
//使用bitCount记录链表元素个数
for (int binCount = 0; ; ++binCount) {
//当走到链表的尾节点
if ((e = p.next) == null) {
//接上新插入的检点
p.next = newNode(hash, key, value, null);
///判断链表元素数量是否已经大于等于树化计数阈值
//这里要进行减一是因为新插入了一个键值对
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//大于等于计数阈值,进行树化
treeifyBin(tab, hash);
break;
}
//在链表遍历的过程
//也要逐个进行哈希和Key比对,因为也有可能是后面这些节点发生相同情况
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//p记录当前节点,去找下一个节点
p = e;
}
}
//如果e不为null
//就代表有节点的key与插入的键值对的key相同
//需要进行替换
//因为如果不需要进行替换,就代表没有相同key
//也就是上面的for循环遍历到了尾节点了
//遍历到尾结点的条件是e == null
if (e != null) { // existing mapping for key
V oldValue = e.value;
//先判断是否关闭了onlyIfAbsent
//再判断旧的值是否为null
//所以onlyIfAbsent其实是用来控制发生key冲突是否替换值的
if (!onlyIfAbsent || oldValue == null)
//替换为新值
e.value = value;
//调用afterNodeAccess方法
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//版本进行更新
++modCount;
//判断是否需要扩容
//注意,这里是先新增键值对然后再扩容
//而jdk1.7是先进行扩容再新增键值对
if (++size > threshold)
resize();
//调用afterNodeInsertion方法
//evict是另一个false
afterNodeInsertion(evict);
return null;
}
可以看到这个putValue挺复杂的,步骤如下
-
先判断底层数组是否为null或为一个空数组,这是针对第一次的put
- 如果是,要进行第一次的扩容
-
计算哈希值获取对应的索引值,判断底层数组该索引是否已经存有对应键值对
- 如果没有,调用newNode方法进行插入
-
如果有,就代表发生了哈希冲突
-
先判断第一个位置,也就是数组里面存储的键值对的哈希值或者key是否和插入的键值对相等
- 如果相等,记录这个节点
- 如果不相等,可能下面的节点会发生key相等
- 判断第一个节点是不是树节点(使用instanceof),如果是就调用putTreeVal方法
- 如果不是就代表是链表形式,遍历链表来让每个节点hash值和key与插入键值对进行对比,同时记录链表的元素个数
- 先判断是否达到树化的计数阈值,如果达到,进行treeifyBin,跳出循环
- 判断是否出现相同的key,如果出现,跳出循环
-
判断替换节点是否为null
- 如果不为null,再判断是否关闭了onlyIfAbsent,或者旧的value是否为null
- 如果关闭了onlyIfAbsent和旧的value也不为null,就进行值替换为新值,而key不变
-
-
自增modCount,版本数加1
-
size自增,元素数量加一
-
判断size是否大于扩容阈值,如果大于进行resize扩容**(与jdk1.7不同,jdk1.7是扩容了之后再进行新增键值对,而jdk1.8是新增了键值对之后再扩容)**
treeIfBin
接下来我们看是如何进行树化的
源码如下
步骤如下:
-
判断底层数组是否为空,接下来判断底层数组的长度是否满足最小的树化计数阈值MIN_TREEIFY_CAPACITY
- 如果不满足,单纯进行扩容,此时树化的操作可能在扩容操作里面实现
-
如果满足树化条件(底层数组的长度满足最小的树化计数阈值)
-
计算出需要树化的链表所在的索引值
-
循环将Node结点转化为TreeNode(对应的是replacementTreeNode,其实这个方法就是简单地使用Node的信息去生成一个TreeNode)
-
这里循环的操作是生成了一个双向链表,采用尾插法,并且hd记录了链表的头结点
-
最后判断头结点是否为空,也就是这个双向链表是否为空
- 如果不为空,对底层数组执行treeify操作
-
所以,treeIfBin这个方法是让不满足树化条件的链表进行扩容,满足树化条件的链表变成一个双向链表
这里为什么要生成一个双向链表呢???
treeify
接下来我们看看真正的树化操作
可以看到,树化操作是使用链表来生成红黑树的
/**
* Forms tree of the nodes linked from this node.
* @return root of tree
*/
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)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> 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);
}
resize
接下来我们看看是如何resize扩容的
源码如下
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//获取当前数组的容量
//并且针对第一次扩容,将容量设为0
//如果是无参构造,oldTab是为null的
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//记录当前扩容阈值
//如果是第一次扩容,这个阈值就是数组的容量
//如果是无参构造,这个threshold为0
int oldThr = threshold;
//开两个变量来记录新的容量和新的扩容阈值
int newCap, newThr = 0;
//如果旧的容量大于0,代表不是第一次扩容
if (oldCap > 0) {
///先判断旧的容量是否已经是允许的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//如果已经是最大值,就不进行扩容了
//不过要改变扩容阈值为Integer的最大值
//因为是Integer的最大值,而MAXIMUM_CAPACITY小于Integer的最大值
//下次就不会触发扩容
threshold = Integer.MAX_VALUE;
//因为不进行扩容,直接返回旧的数组
return oldTab;
}
//如果旧的容量没有超过允许的最大值,那么就可以进行扩容
//同理,扩容的规则为原来的两倍
//扩容完后,判断是否为允许的数组最大值,
//而且还要判断旧的容量是否要大于等于默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//因为新容量为旧容量两倍
//所以扩容阈值也相应变为2倍
newThr = oldThr << 1; // double threshold
}
//如果旧的容量不大于0,也就是为0
//而且旧的扩容阈值不为0
//代表的也是第一次插入
//注意,构造方法里面是将初始化容量存储进threshole的
else if (oldThr > 0) // initial capacity was placed in threshold
//所以,这里只需要让初始化容量为threshole即可
newCap = oldThr;
//如果进入在这里
//就表明是无参构造,而且是第一次put
else { // zero initial threshold signifies using defaults
//新容量为默认的16
newCap = DEFAULT_INITIAL_CAPACITY;
//扩容阈值为默认容量乘上负载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//现在已经初始化好容量了
//接下来是初始化新的扩容阈值
//这里是针对第一次put的
//在扩容为2倍后,新的扩容阈值newThr也是扩容了两倍,这里不必再进行
if (newThr == 0) {
//让扩容阈值为新的容量乘上复杂因子
float ft = (float)newCap * loadFactor;
//判断扩容阈值是否小于允许最大的底层数组大小
//如果不小于,就让扩容阈值变为Integer最大值,不让下一次扩容进行
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;
//使用for循环来遍历底层的数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//遍历找到不为空的位置
if ((e = oldTab[j]) != null) {
//将这个位置设为空,让gc可以回收不要的引用
oldTab[j] = null;
//判断这个位置之前是否产生了冲突
//如果没产生冲突,就代表这个位置只有这个键值对
if (e.next == null)
//不需要重新hash,使用旧的hash值来与上新的底层数组长度
//获得新的索引位置
//让新数组的这个位置存储这个值
newTab[e.hash & (newCap - 1)] = e;
//如果这个位置发生了树化现象
//调用树节点的split方法
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果不是树节点,而且后面节点不为null
//代表形成了链表
else { // preserve order
//这里会要形成两个链表、
//一个链表是索引值没变的元素组成
//另一个链表是索引值要加上旧的容量的元素组成
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//循环遍历这个链表
do {
next = e.next;
//索引值不变的结点放在loHead链表上
if ((e.hash & oldCap) == 0) {
//采用的是尾插法
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//索引值需要改变的结点放在hiHead链表上
//采用的是尾插法
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//loHead是索引值不变的链表,所以直接放在原位置上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//hiHead是索引值需要改变的链表,放在原位置的后面oldCap位置上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//最后结束
//返回新的数组
return newTab;
}
可以看到,resize方法挺复杂的,主要涉及到三个方面
- 计算新的容量(2倍)
- 计算新的扩容阈值(2倍)
- 让底层数组指向新容量、新扩容阈值的数组
- 新数组复制旧数组里面的内容(如果是树结点就执行split,不冲突的进行重新计算索引值即rehash,根据位置是否变化生成链表)
这里有一个JDK1.7与JDK1.8不同点就是,JDK1.7由于是先进行扩容,然后进行插入新的键值对,对之前旧的元素,是全部遍历然后进行rehash的,而jdk1.8不一样,对于单独一个旧元素会进行rehash,而对于发生哈希冲突产生的链表,里面的结点则会分两种情况,一种是还是放在原位置,另一种情况是要放在后面的位置(原先位置加上旧数组的长度)
这里解释一下为什么
首先,获得索引的方法是哈希值余上数组容量,所以哈希值可以是由数组容量的倍数加上一个低于数组容量的数加起来而成
hashcode = n * capacity + j //公式
现在发生扩容了,新的容量为旧的容量的两倍,就会发生两种情况,这两种情况都是由n倍的旧数组容量产生
hashcode = m * newCap + k //公式
hashcode = m * 2 * oldCap + k = n * oldCap + j
//分两种情况
//当n为偶数的时候,m = n/2,k = j,所以位置是一样的
//当n为奇数的时候,m = n/2是一个小数,不满足
//所以只能让m为n-1的两倍,然后k进行补充
//即m = (n - 1)/2 , k = j+oldCap,
所以新位置可能是与原先的位置一样,也有可能不一样,为原先位置的后oldCap个
关键就在于n是奇数还是偶数
在数组容量为二次幂的前提下,将hashcode化成二进制,同理也是由数组容量的倍数加上一个低于数组容量的数加起来而成,我们就可以不用管低于数组容量的那一部分先,只关注数组容量的倍数那一部分,如果是奇数倍的话,那么这一部分与数组容量进行与运算必定不为0,为数组容量,如果是偶数倍的话,这一部分与数组容量进行与运算一定为0
举个栗子
数组容量为16
16的二进制为10000,那么2 * 16 = 10000+10000 = 10000 ,那么3 * 16 = 10000+10000+10000 = 110000,那么4 * 16 = 1000000,那么5 * 16 = 1010000,可以看到只要是奇数倍,那么化为二进制后,肯定会留有数组容量的二进制,与原来数组容量进行与运算,这后面的一部分一定不会为0
所以就利用这个关系就可以判断n为奇数还是偶数了,所以只需要让hashcode与原来容量进行与运算,判断是否为0,就可以得知n为奇数还是偶数,如果为0,就代表n为偶数,就直接放在原位置,如果不为0,就代表n为奇数,要放在原位置后面的oldCap处
split
接下来我们看看,如果产生哈希冲突形成的结构是红黑树是会进行怎样的处理,也就是对应的split方法,这个方法其实也是用来判断元素位置,只不过这里遍历的对象是红黑树,而且还要考虑退化
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变量来记录两个链表的长度
//如果最后这两个变量小于树化的计数阈值,就要进行红黑树退化
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;
//这里同样是判断元素是否需要改变位置,与前面的判断是一致的
//loHead链表存储n为偶数的情况的元素
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
//插入第一个元素
loHead = e;
else
//使用尾插法,形成链表记录
loTail.next = e;
loTail = e;
//记录个数,可能会进行退化
++lc;
}
//hiHead链表存储n为奇数的情况的元素
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
//使用尾插法
hiTail.next = e;
hiTail = e;
//记录个数,可能会进行退化
++hc;
}
}
//下面这一部分就是进行将旧元素存放进新数组了
if (loHead != null) {
//判断不改变元素的红黑树是否需要进行退化
if (lc <= UNTREEIFY_THRESHOLD)
//调用untreeify进行退化
tab[index] = loHead.untreeify(map);
else {
//不需要退化则要进行树化
//将不变索引的元素放在原先的位置
tab[index] = loHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
//需要重新进行树化
//如果另一棵树为空,代表这课树拥有全部元素
//就不需要重新进行树化,因为本来传进来的就是一棵树
//只不过相当于把树转移进了loHead,然后loHead放进了新数组
if (hiHead != null) // (else is already treeified)
//重新进行树化
loHead.treeify(tab);
}
}
if (hiHead != null) {
//判断改变元素的红黑树是否需要进行退化
if (hc <= UNTREEIFY_THRESHOLD)
//调用untreeify进行退化
tab[index + bit] = hiHead.untreeify(map);
else {
//将索引改变的元素放在改变后的位置
tab[index + bit] = hiHead;
//如果另一棵树不为空,代表这棵树只拥有部分元素
//需要重新进行树化
if (loHead != null)
hiHead.treeify(tab);
}
}
}
总结一下split的步骤
-
使用两棵树来分别记录需要改变位置的结点和不需要改变位置的结点,使用的是树结点,但实际上记录的是一个链表结构,因为TreeNode继承了Node,所以是有链表结构的
-
同时使用两个变量来记录两个链表的长度,后面用来判断是否需要重新树化
-
使用for循环来遍历传进来的树,同理使用与运算来判断该结点位置是否发生变化,对应放在两个链表上
-
最后对这两个链表进行处理,判断是否需要退化和树化,因为头结点是一个树结点,不单单有链表结构也有树结构,而查询的时候,会将其判断为树结构,所以这里要判断是否需要进行退化
- 通过比较链表的长度和树化的计数阈值,如果达不到,就要进行退化,执行untreeify方法
- 如果达到树化结构,再判断是否需要重新树化,即原来的树是否分成了不同的两个链表,即对应会形成两棵不同的树
- 如果另一个链表是个空,那么就代表其没有分成两棵树,不需要进行重新树化
- 如果另一个链表不为空,那么就代表分成了两棵树,需要进行重新树化(根据底层的链表进行重新树化)
至此,split方法结束
afterInsertion
可以看到这个方法什么都没做,可能是方便以后的拓展的
至此一个Put就到此结束了
总结
-
无参构造函数单纯初始化了loadFactory为默认的负载因子0.75
-
扩容操作分为4步
- 计算新容量
- 计算新的扩容阈值
- 让底层数组指向新容量的数组
- 将旧数组的内容复制到新数组中去
-
树化操作不仅是要满足冲突达到树化的计数阈值,而且要底层数组长度也要满足树化容量的计数阈值,不满足后者只会进行一个扩容操作
-
rehash是让原hashcode重新与上底层数组最大索引
-
底层数组长度为二次幂是因为只有这样才可以让rehash的与运算等效于原hashcode对底层数组长度进行取余
-
当扩容的时候新增加元素,元素可能放在旧的数组也可能放在新的数组,因为扩容步骤有4步,如果执行到让底层数组指向新数组之前,则是添加到旧数组,如果执行到底层数组指向新数组之后,则是添加到新数组,从而可能产生并发问题。