HashMap中的resize()方法源码解读(基于jdk1.8)
resize()方法的用法
- 初始化HashMap
- 当容量的大小到达阈值时进行扩容
关于是进行初始化还是进行扩容在resize方法里面会进行判断从而进行相关操作
整个resize方法分为两部分
第一部分,确定是本次操作是需要进行扩容还是进行初始化,然后根据实际情况确定最新的阈值与容量
然后就是扩容(关于扩容本次仅讲解一下链表的相关操作,红黑树部分,后续有时间会继续进行讲解)(具体讲解可以继续往下看,话不多说看源码)
resize第一步:确定扩容还是初始化,确定最新容量与阈值
final Node<K,V>[] resize() {
//oldTab存储HashMap扩容前的旧值(若运行resize()前并未初始化,则table = null)
Node<K,V>[] oldTab = table;
//旧值数组的长度(HashMap是数组+链表或者数组+红黑树)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧值的阈值(阈值,当hashMap的数组长度到达阈值时,需要进行扩容)(若运行resize()前并未初始化,则threshold为HashMap桶容量,可通过观看构造函数得到此结论)
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//已经初始化过(现在HashMap当中有值),则进行的是扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {//容量已经最大,无法扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
//1.将旧值容量进行扩容(使用向左唯一一位(旧容量乘以2))
//2.若扩容之后的容量满足小于最大容量并且旧容量值大于小于默认的容量(16)新的阈值则为旧阈值扩大一倍(必须满足这两个条件)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//若没有经历过初始化,并且在使用时通过构造函数指定了initialCapacity, 则table大小为threshold, 即大于指定initialCapacity的最小的2的整数次幂(可以通过构造函数得出)
else if (oldThr > 0)
newCap = oldThr;
else {
//若没有经历过初始化,并且没有通过构造函数指定initialCapacity,则赋予默认值(数组大小为16,加载因子为0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//上述方法(扩容或者初始化)走完,完成对容量的操作但是并没有指定阈值(正常将容量扩容或者初始化时指定了initialCapacity),计算阈值(最后的容量*加载因子)
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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;
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;
}
}
}
}
}
return newTab;
}
resize第二步:扩容
//根据新的容量重新定义一个数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新建立数组赋值到HashMap成员变量
table = newTab;
//若存在旧数据则进行后续数据迁移
if (oldTab != null) {
//因为HashMap是数组+链表或者数组+红黑树(根据数组下标找寻到相应链表或者红黑树)遍历原本数组,找到相应的链表或者红黑树进行操作
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//数组[j]有数据,进行后续操作
//将数组[j]下代表的链表或者红黑树的头结点(根节点)赋予 e
if ((e = oldTab[j]) != null) {
//将旧数组[j]处置为空
//我认为这两步的操作就是将原本数组下标处的数据从原本位置挪出,接下来操作此链表即可
oldTab[j] = null;
if (e.next == null)
//就只有一个头结点(根节点)直接就可按照e.hash & (newCap - 1)的计算法,计算出相应在新table里的位置进行插入
//与put方法里面的操作一致
newTab[e.hash & (newCap - 1)] = e;
//这里先不讲红黑树的部分了,后续有时间会继续发博客讲解
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//首先定义五个变量(Tail单词意思是尾部的意思)所以我们可以这么理解
//loHead lo头部 loTail lo尾部
//hiHead hi头部 hiTail hi尾部(暂且这么记忆)
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;
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;
}
}
}
}
}
return newTab;
}
咱们将上方未讲解代码分为两个部分
- do while部分
- 两个if判断
第一部分: do while部分
//整个do while(不看具体实现),我认为就是遍历整个链表
//内部相关解释看内部源码注释
do {
next = e.next;
//咱们先不管 (e.hash & oldCap) == 0 这个条件,反正就是符合这个条件就进行lo相关变量的操作,不符合进行hi相关变量的操作
if ((e.hash & oldCap) == 0) {
if (loTail == null)//这部分很像将一个节点插入链表所需要做的操作(下方图1右侧是只有一个数据时,下列代码最终形成的状态,左侧是多个数据时的最终形成的状态)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)//同lo相关操作
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//综合看来,根据(e.hash & oldCap) == 0 这个条件进行筛选,将原本的一个链表划分成两个链表
} while ((e = next) != null);
图1:
第二部分: 两个if判断
//第一部分进行完毕之后,形成两个链表,接下来就是将两个链表分别插入不同位置
//至于插入位置,后边会有讲解
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
关于 (e.hash & oldCap) == 0 与第二部分中的 两个链表插入位置
咱们看完是确定条件之后是如何拆分的相关代码之后,来聊聊这个判断条件
在扩容时,我们是需要计算扩容后元素应该放到新数组当中什么位置,我们来看看源码当中是怎么写的
首先要明确HashMap的容量一定是2的整数次幂
假设当前容量是默认的16
(容量 - 1) & hash 是put方法里面确定插入元素属于哪个数组下标
16-1 = 15 的二进制为:0000 0000 0000 0000 0000 0000 0000 1111
这个时候与hash值进行"&"计算,算出相应数组下标,决定因素在后四位
16 扩容后 是 32
32-1 = 31 的二进制为:0000 0000 0000 0000 0000 0000 0001 1111
这个时候与hash值进行"&"计算,计算相应数组下标,决定因素在后五位
二者区别就在第五位上
假设现在有一个插入元素的key的hash值二进制第五位是0,进行相应计算,最后计算结果二者是一样的。
也就是说假如咱们一个一个的去计算元素所应该在什么位置时,当元素的key的hash值第五位是0 这个时候放置的位置与之前未扩容之前是一样的。
再次假设现在有一个插入元素的key的hash值二进制第五位是1,进行相应计算,最后计算结果二者相差16,也就是说第一个计算出来的数组下标加上16就是第二计算出来的数组下标。
也就是说,我们可以进行一次判断,判断元素的hash值第五位是0还是1,0则不变还是之前的数组下标即可,1的话就是原本的数组下标加上16就是在新数组的新下标
只需要看第五位其余位置无所谓,那么就让其他位置为0,第五位为1(这不就是扩容前容量16吗),然后与元素hash值做"&"操作(0 & 0 = 0;0 & 1 = 0 ;1 & 1 = 1)
这不就是咱们所看的这个判断条件吗(e.hash & oldCap) == 0
所以在两个if判断那里,将两个链表一个放到 j 一个放到 j+oldCap 位置
想说的就这么多,本人水平有限,如果有错误欢迎大佬指教。