一般情况下,第一次添加元素或元素数量超过阈值时便会触发扩容
每次扩容的都是之前容量的2倍,第一次默认是16
容量有上限,必须小于 1<<30,即1073741824。若容量超出了这个 数,则不再增长,且阈值会被设置为 Integer.MAX_VALUE
JDK7中
空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数组
有参构造函数:根据参数确定容量、负载因子、阈值等
第一次put时会初始化数组,其容量变为不小于指定容量的2的幂数,然后根据负载因 子确定阈值
如果不是第一次扩容,则 新容量=旧容量 x 2 ,新阈值=新容量 x 负载因子
JDK8
空参构造:实例化HashMap默认内部数组是null,即没有实例化。第一次调用put()时开始第一次初始化扩容,长度为 16
有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数, 将这个数设置赋值给阈值(threshold)。第一次调用put()时,会将阈值赋值给容量, 然后让 阈值 = 容量 x 负载因子
若不是第一次扩容,则容量变为原来的2倍,阈值也变为原来的2倍。(容量和阈值都变为原来的2倍时,负载因子还是不变)
此外还有几个细节需要注意
首次put时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
不是首次put,则不再初始化,直接存入数据,然后判断是否需要扩容
扩容是不会从新计算hash值的,因为已经缓存在每一个节点中了
resize()扩容方法
final Node<K,V>[] resize() {
// 旧数组
Node<K,V>[] oldTab = table;
// 第一次添加元素第一次扩容oldTab是null,oldCap是0,要不然返回当前数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧容量对应的需要扩容的阈值
int oldThr = threshold;
// 新容量,新容量对应的需要扩容的阈值
int newCap, newThr = 0;
if (oldCap > 0) { // 旧的map有元素
if (oldCap >= MAXIMUM_CAPACITY) { // 如旧map的容量大于1<<30,设置下次扩容的阈值,
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果扩容后的新容量小于HashMap最大容量,且旧容量大于等于初始化容量
// 这块也可看到HashMap的扩容原理,即newCap=oldCap << 1,也就是2倍
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是16
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);
}
// 把newThr值给threshold,下一次扩容需要达到元素个数
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 第一次添加元素初始化数组大小是16
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) { // 链表不是空的,用变量e存储链表
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;
// oldCap是2的幂次方,e.hash&oldCap的结果要么是0,要么是oldCap
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; // 低位链表位置
}
}
}
}
}
// 如果是第一次添加元素,直接返回16大小的数组
return newTab;
}
===========================================
jdk1.7扩容时候有线程安全死循环问题
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历数组
for (Entry<K,V> e : table) {
// 存有数据,不为空
while(null != e) {
// 存储下一个节点
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];
// 让新数组的新index的位置的元素是e,也就是把e节点迁移到了新的链表上
newTable[i] = e; // *线程1在这行暂停(尚未执行该行)
// 循环下一个节点
e = next;
}
}
}
多线程扩容时,因为共享原来的数据,扩容后会形成循环链表
在get()时get这个链表上不存在的数据,就会死循环在环形链表中