外观
HashMap 的扩容机制是怎样的
HashMap 简介:
HashMap 是 Java 中常用的哈希表实现,它允许存储键值对,并且可以根据键快速查找对应的值。HashMap 内部使用数组和链表(或红黑树)来存储数据,通过哈希函数将键映射到数组的某个位置,当发生哈希冲突时,使用链表或红黑树来解决冲突。
扩容机制概述: HashMap 的扩容机制主要涉及两个重要的参数:初始容量(initialCapacity)和负载因子(loadFactor)。初始容量是指 HashMap 在创建时数组的初始大小,默认值为 16;负载因子是指当 HashMap 中的元素数量达到数组容量的一定比例时,就会触发扩容操作,默认值为 0.75。当 HashMap 中的元素数量超过阈值(threshold,阈值 = 容量 * 负载因子)时,就会进行扩容操作,将数组的容量扩大为原来的两倍,并重新计算每个元素在新数组中的位置。
扩容触发条件:
HashMap 在以下两种情况下会触发扩容操作:
插入元素后元素数量超过阈值:在调用 put 方法插入元素时,会检查当前元素数量是否超过阈值,如果超过则触发扩容。
链表长度达到阈值且数组长度达到 64:当链表长度达到 8 且数组长度小于 64 时,会先进行数组扩容;当链表长度达到 8 且数组长度达到 64 时,会将链表转换为红黑树。
扩容步骤:
计算新容量和新阈值: 当触发扩容时,HashMap 会将数组的容量扩大为原来的两倍,并重新计算新的阈值。例如,原来数组容量为 16,阈值为 12(16 * 0.75),扩容后数组容量变为 32,新的阈值变为 24(32 * 0.75)。
创建新数组: 根据新的容量创建一个新的数组,用于存储扩容后的数据。
重新哈希并迁移元素: 遍历原数组中的每个元素,对于每个元素,重新计算其在新数组中的位置,并将其迁移到新数组中。具体来说,对于链表节点,会根据其哈希值和新数组的长度重新计算索引,将其插入到新数组的对应位置;对于红黑树节点,会将红黑树拆分成两个链表,分别插入到新数组的对应位置,如果拆分后的链表长度小于等于 6,则会将链表转换回普通链表。
代码示例及解释 以下是 HashMap 扩容相关的部分源码及解释:
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;
}
代码解释:
- 计算新容量和新阈值:根据原数组容量和阈值计算新的容量和阈值。
- 创建新数组:根据新的容量创建一个新的数组。
- 迁移元素:遍历原数组中的每个元素,根据元素的类型(普通节点、红黑树节点)进行不同的处理,将元素迁移到新数组中。
总结:
HashMap 的扩容机制是为了保证哈希表的性能,当元素数量超过阈值时,通过扩容操作将数组容量扩大为原来的两倍,并重新计算元素在新数组中的位置。扩容操作会带来一定的性能开销,因此在创建 HashMap 时,可以根据实际情况合理设置初始容量和负载因子,以减少扩容的次数。