4、HashMap如何扩充?
-
首先看下put,通过传入hash值,key,value
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
-
然后看下putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //新建一个tab指向hash表,
if ((tab = table) == null || (n = tab.length) == 0) //如果是空表
n = (tab = resize()).length; // 执行扩容方法,并把扩容后的hash表长度给n
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //如果size大于阈值,哈希表就要进行扩充
resize();
afterNodeInsertion(evict);
return null;
}
-
HashMap的扩容方法
final Node<K,V>[] resize() { //由此可见,扩容后将扩容后的返回去
Node<K,V>[] oldTab = table; //将hash表赋值给oldTab
int oldCap = (oldTab == null) ? 0 : oldTab.length; //如果旧表是空,容量旧容量为0,如果不是空,取旧表的长度
int oldThr = threshold; // threshold 阈值,下次需要扩容时的值,阈值=容量*加载因子
int newCap, newThr = 0; //定义新的容量,新的阈值为0
if (oldCap > 0) { // 如果旧容量大于0
if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的容量>=最大容量: 1<<30(2的30次方)
threshold = Integer.MAX_VALUE; //将Integer最大值给到阈值
return oldTab; //返回旧表
} //若旧容量< 最大容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //如果旧容量乘以2 < 最大容量(新容量为旧容量的两倍) 并且 旧容量>= 默认初始化容量(16)
oldCap >= DEFAULT_INITIAL_CAPACITY) //将旧的阈值增加一倍赋值给新的阈值
newThr = oldThr << 1; // double threshold
} // 下面为旧容量为0,并且阈值>0, 说明之前创建了hash表,但没有添加元素,将初始化容量 = 阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr; //将阈值给到新的容量
else { // 旧容量和旧的阈值都是0, 说明还没有创建hash表
newCap = DEFAULT_INITIAL_CAPACITY; //否则使用默认容量 和默认阈值 容量*加载因子(0.75)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //
}
===================上面操作执行完成后=================================
if (newThr == 0) { // 如果新的阈值为0,就用新的容量*加载因子再算一次
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? //如果新的容量<最大容量 并且 新的加载因子<最大容量
(int)ft : Integer.MAX_VALUE); // true:使用新的加载因子,false:使用Integer的最大值
}
threshold = newThr; // 更新阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的链表数组,容量为原来的两倍
table = newTab; //把新的哈希表给到旧hash表
// 接下来就要遍历复制了
if (oldTab != null) { // 如果旧的哈希表不是空
for (int j = 0; j < oldCap; ++j) { //遍历旧的容量
Node<K,V> e;
if ((e = oldTab[j]) != null) { //旧表当前元素(桶)赋值给e,并且不是null
oldTab[j] = null; //旧表当前元素赋值为空(应该是为了垃圾回收)
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);
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; //最后将扩容完的哈希表返回
}
扩容过程中几个关键的点:
-
阈值:当达到阈值时,哈希表就进行扩充
-
新初始化哈希表时,容量为默认容量,阈值为 容量*加载因子
-
已有哈希表扩容时,容量、阈值均翻倍
-
如果之前这个桶的节点类型是树,需要把新哈希表里当前桶也变成树形结构
-
复制给新哈希表中需要重新索引(rehash),这里采用的计算方法是e.hash & (newCap - 1),等价于 e.hash % newCap
结合扩容源码可以发现扩容的确开销很大,需要迭代所有的元素,rehash、赋值,还得保留原来的数据结构。
所以在使用的时候,最好在初始化的时候就指定好 HashMap 的长度,尽量避免频繁 resize()。
参考大佬的博客: 大佬的博客