HashMap的put方法和resize方法的源码详解
概要
注:该文章主要讲解HashMap的源码,阅读该部分前最好先阅读一下putVal方法的源码。
- 本文的代码均为jdk1.8,java.util包下的HashMap源码
- 本文主要讲解resize方法中的关键代码,部分细节可能会省略,需要读者自己去阅读源码进行补充。
resize整体代码
- 阅读注释
final Node<K,V>[] resize() {
/**
计算旧Node数组的容量和阈值(阈值即触发扩容的阈值,阈值一般为Node数组容量 * 负载因子)。
准备计算新Node数组的容量和阈值
**/
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/**如果oldCap大于0,说明旧Node数组是已经初始化过的,这种情况发生在执行putVal方法时
旧Node数组的容量达到了阈值,触发了resize方法
**/
if (oldCap > 0) {
//如果旧数组就已经达到了最大的容量,则让阈值也变为最大容量,直接返回旧数组
//实际上是没有发生扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//常规情况下会执行这条if。新的数组容量等于旧数组的两倍
//如果旧数组阈值已经达到HashMap的默认容量大小(16),则新阈值也变为旧阈值的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
//HashMap在执行构造方法后,只是单纯为自己的成员变量(threshold、loadFactor)赋值
//并没有初始化Node数组,真正初始化Node数组实际上是在putVal方法中
//该if条件就是处理HashMap(int)和HashMap(int,float)这两个指定map大小的构造函数的
//让新数组容量等于阈值(该阈值在构造方法中赋值为容量大小)。
else if (oldThr > 0)
newCap = oldThr;
//该if条件用于处理Hashmap()不指定map大小的情况
//新容量大小等于默认大小(16),新阈值等于容量 * 默认负载因子(0.75)
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//以上有几种情况会导致newThr为0
//主要是在指定map大小构造函数场景下进行put时会进入下面这个if
//如果newThr = 0,则newThr = 新的容量 * 负载因子
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//用新的容量大小创建一个Node数组并赋值给table成员变量
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//开始进行数据迁移
if (oldTab != null) {
//遍历旧Node数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//断开oldTab[j]对e指向node的引用
oldTab[j] = null;
//如果该哈系桶只有一个node,则e的next就是null。
//用新数组大小将e映射到新数组的对应的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果e实际上是一颗红黑树的情况,此处篇幅较大,跳过
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/**
如果e为一个链表的情况
以下的处理将旧node数组中一个哈希桶中的一个链表根据node的hashcode & oldcap
的值是否为0,划分为两个链表分散到新node数组的不同哈希桶,目的是为了让新数组
中的单个哈希桶的node数不要过多,会影响检索效率。
**/
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;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//将两个链表分散到新node数组的不同位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
结论
- HashMap的构造函数并不会初始化Node数组,初始化会延迟到putVal方法里进行。