-
扩容机制
- 何时扩容
当元素个数size>threshold=cap*loadFactor
size指的是key value的元素个数, cap指hashmap的table数组的长度(不包括链表),loadFactor指负载因子,默认0.75 - 扩容关键参数
cap,threshold,size
默认初始大小16,负载因子0.75
cap threshold size 16 12 0 32 24 13 64 48 25 128 96 49 256 192 97 512 384 193 1024 768 385 2048 1536 769 4096 3072 1537 8192 6144 3073 16384 12288 6245 当size>threshold的时候,会发生扩容,扩容之后的容量cap是上一次的两倍,threshold也是上一次的两倍,由于都是两倍,所以threshold=cap*loadFactor这个等式在扩容后仍然成立.
- 元素个数和初始容量的公式
当已知map元素个数是x的情况下,如何设置初始化容量?
HashMap map = new HashMap(int initialCapacity);
由于threshold=cap乘以loadFactor<元素个数的时候会扩容,所以为了不扩容,必须满足:
所以 threshold=cap*loadFactor>=元素个数
所以 cap>=元素个数/loadFactor
cap是大于或者等于(元素个数/loadFactor)的最小的2的n次幂
综上所述:
当负载因子是0.75的时候,
元素个数和初始化参数有如下关系:
- 何时扩容
元素个数 | threshold | 初始化个数 |
---|---|---|
13 | 17.7 | 17-32 |
96 | 128 | 65-128 |
10000 | 13333.3 | 8193-16384 |
这个公式比较难表达出来
initialCapacity的最大值是大于或者等于(元素个数/loadFactor)的最小的2的次方.假设这个数是maxInitialCapacity
initialCapacity的最小值是maxInitialCapacity/2+1;
initialCapacity是maxInitialCapacity/2+1的时候,数组的初始大小会自动扩展到maxInitialCapacity;
-
阿里规约
initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为0.75, 如果暂时无法确定初始值大小,请设置为16(即默认值)。 -
谷歌guava工具类
com.google.common.collect.Maps#newHashMapWithExpectedSizepublic static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) { return new HashMap(capacity(expectedSize)); } static int capacity(int expectedSize) { if (expectedSize < 3) { CollectPreconditions.checkNonnegative(expectedSize, "expectedSize"); return expectedSize + 1; } else { return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647; } }
如果用Maps#newHashMapWithExpectedSize,expectedSize指定为元素个数即可
-
扩容过程解析,java7 vs java8
- java7扩容的时候会遍历table的Entry,每个Entry里再遍历链表.由于扩容的时候,hashCode不变,但是hashSeed发生变化,java7的hash依赖于hashCode和hashSeed,所以java7每个节点的hash都要重新计算,并且链表的节点顺序会逆转.
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;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
- java8扩容的时候也是遍历table的Entry,每个Entry里再遍历链表.但是java8的hash只依赖于hashCode,扩容的时候hashCode不变,所以hash不变,不需要重新计算.同时java8做结点重排序的时候有一个巧妙的算法.即结点的hash不变,newCap=oldCap*2的时候,结点X在新map中的位置要么等于原来的位置p,要么等于p+oldCap,判断条件是(e.hash & oldCap) == 0是否满足,满足则在原位置p,不满足则在p+oldCap
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;
}
}
}
}
具体的分析可参考:https://blog.youkuaiyun.com/qq_37113604/article/details/81353626