(注:本文基于JDK1.8)
前言
在Java之HashMap源码分析(第四篇:扩容机制)文章中,我已经做了扩容的源码分析,遗憾的是,扩充容量的许多细节没有提及,所以才有这篇文章的详细分析,先一起看下在什么情况下,resize()方法会在HashMap中被调用,我将依照HashMap.java源文件中resize()方法被调用的先后顺序进行描述
1、添加多个元素时
HashMap添加一个Map集合中的所有元素时,如果即将添加的Map集合中的所有元素的数量大于HashMap对象持有的扩容阈值,此时resize()方法会被调用
2、第一次添加元素时
HashMap对象创建后,第一次添加元素时,resize()方法会被调用
3、添加一个新的元素后
HashMap添加一个新的元素后,如果HashMap对象已经持有的元素总数大于当前扩容阈值threshold,resize()方法会被调用
4、桶中的单链表太长需要转红黑树时
哈希冲突太多,桶内的单链表结构即将转为红黑树结构的方法中,如果HashMap对象持有的底层数组对象的长度小于64个,resize()方法会被调用
5、computeIfAbsent()方法添加元素时
通过computeIfAbsent()方法添加元素时,若当前元素总数大于扩容阈值或者底层数组仍未初始化时(table为null)或table长度为0(HashMap第一次添加元素时,底层数组才有可能为null),resize方法会被调用
6、compute()添加元素时
通过compute()方法添加元素,当前元素总数大于扩容阈值或者底层数组仍未初始化时(HashMap第一次添加元素底层数组才可能为null)或者底层数组table的长度为0,resize方法会被调用
7、合并添加的元素时
通过merge()方法合并元素时,若当前元素总数大于扩容阈值或者底层数组仍未初始化时(HashMap第一次添加元素底层数组才可能为null)或者底层数组的长度是0时,resize方法会被调用
共计7处调用resize()方法……
resize()方法分析
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; // 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);
}
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)
((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()方法是HashMap扩容时调用的方法,所谓扩容,就是将HashMap对象持有的数组对象,由容量小的数组对象,换成一个容量更大的数组对象,这样HashMap依赖这个数组对象就能保存更多的元素了!
resize()方法实现过程(简述):
1、创建局部变量,保存临时数据
2、根据旧数组的容量、旧数组的扩容阈值去确定新数组容量、新数组的扩容阈值
3、创建新的数组对象,并由HashMap对象持有的实例变量table负责保存
4、旧数组不为null时,将旧数组中的所有元素全部转移到新数组中,转移过程中,会在旧数组中一个桶一个桶的遍历元素
5、如果扩容成功,则resize()方法的返回值为新创建的数组对象
…………以下内容为resize()方法的详细分析…………
一、创建局部变量,保存数据
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
局部变量oldTab负责持有旧数组对象table
局部变量oldCap负责持有旧数组的容量,当旧的数组对象oldTab还未创建时,初始值为0,如果oldTab已经创建,则获取此数组的长度
局部变量oldThr负责持有旧数组的扩容阈值threshold
局部变量newCap负责保存新数组的容量
局部变量newThr负责保存新数组的扩容阈值
二、根据旧数组对象的容量、旧数组对象的扩容阈值,计算出新数组的容量、新数组的扩容阈值
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; // 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);
}
1、判断旧数组容量是否大于0,此时会出现两种情况
第一:旧数组容量大于等于最大容量(MAXIMUM_CAPACITY),则将int的最大值赋值给HashMap对象持有的当前扩容阈值threshold,然后整个resize方法会结束,并return旧的数组对象
第二:将旧数组容量oldCap左移1位,计算出的新值会赋值给新数组容量newCap,此时新数组容量newCap是旧数组容量oldCap的两倍,然后判断新数组容量newCap是否小于MAXIMUM_CAPACITY,如果为true,会继续判断旧数组容量oldCap是否大于等于默认初始化容量16(DEFAULT_INITIAL_CAPACITY),若也为true,就会将新数组扩容阈值newThr也赋值为旧数组扩容阈值的两倍
2、旧数组数量如果小于等于0时且旧数组扩容阈值oldThr大于0,则会用旧数组的扩容阈值oldThr赋值给新数组的容量newCap,还记得湖边的夏雨荷吗?当使用两个参数的构造方法创建一个HashMap对象时,其中有这么一句让人摸不着头脑,它对传入的初始化容量做了一个保护,找到一个与比它大,且最接近2的n次方的数字,然后把结果赋值给了threshld,这里竟然给扩容阈值threshold做了一个赋值,没想到作者会在resize方法里利用该threshld值赋值给新数组的容量newCap
3、最后一种情况则是数组容量小于等于0且初始阈值threshold也等于0的情况,这时新数组容量newCap赋值为默认值16,新数组阈值newThr赋值为16 * 0.75,即12
三、将计算好的新数组阈值newThr、新创建的数组对象newTab、分别由HashMap对象持有的实例变量threshold与table各自保存上
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
1、HashMap对象持有的实例变量threshold负责持有新数组的扩容阈值newThr
2、根据已经计算好的新数组的容量newCap,创建一个大小为newCap的数组对象,并由局部变量newTab暂时持有
3、HashMap对象持有的实例变量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;
}
}
}
}
}
遍历旧的数组对象,判断桶中是否持有元素(HashMap对象持有的数组对象,此时数组的每个下标称为【桶】),旧数组桶中持有的元素会添加到新的数组中,新的数组中将包含旧数组中的所有元素(注:oldTab此时不为null,若oldTab未指向一个数组对象,resize方法将会直接返回新的数组对象newTab)
1、桶内没有元素
此时代码块结束,会遍历下一个桶
2、桶内有元素的三种情况(每次会先将旧数组的桶下标处先赋值为null,然后再根据桶内的元素情况做处理)
first、桶内是一个元素
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
取出桶内Node对象e的保存的hash值,与新的数组容量-1作一个按位与计算,计算的结果是一个新数组中的桶下标,利用新计算出来的桶下标将Node对象赋值到新的数组中
second、桶内是红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
桶内取出的是TreeNode类的对象时,说明为红黑树结构
由红黑树结点对象的split方法负责完成旧数组红黑树结构迁移到新数组中的工作(参见HashMap红黑树篇)
third、桶内是单链表
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;
}
}
首先定义5个局部变量:loHead、loTail、hiHead、hiTail、next,均是Node类型,lo开头的为一组、hi开头的为另外一组、next单独存在,作者为什么要定义这5个局部变量?
do……while循环开始……局部变量next负责存储当前节点对象的下一个节点对象e.next,接着会将每个当前节点对象e持有的hash与旧数组容量oldCap作一个按位与计算,按位与计算的结果会出现两种情况:0与非0
当按位与的结果为0时,由loHead、loTail负责持有单链表中的节点对象(loHead的作用是指向头结点、loTail的作用是指向尾节点)
当按位与的结果为非0时,由hiHead、hiTail负责持有单链表的节点对象(hiHead指向头结点、hiTail指向尾节点)
上面的行为,如果都有命中,那么旧数组桶中的一个单链表就会被分割成两个单链表
在旧数组中的单链表循环结束后,loHead、hiHead分别持有的单链表会分别放到新数组中的桶中,loHead持有的单链表会放到新数组中与旧数组相同的桶下标j处,而hiHead持有的单链表则会放到新数组中的j+oldCap桶下标处(下图为可能出现的分割单链表情况展示)
总结
1、旧的数组容量大于MAXIMUM_CAPACITY……2^31,不会再创建新的数组对象,否则resize方法一定会返回一个新的Node数组对象
2、新创建的数组对象容量一定是旧数组的2倍
3、新的数组对象,它的扩容阈值一定是旧的数组对象扩容阈值的2倍
4、红黑树结构的处理情况,需要单开文章总结,因红黑树相对复杂……
5、较长的单链表在扩容时是可能会被分割成两个单链表的