HashMap详解-内部实现原理(3)-扩容机制

HashMap在元素超过负载因子与容量的乘积时触发扩容,以维持性能。扩容涉及创建新数组,重新计算元素位置,减少冲突,提高空间利用率。尽管扩容能维持效率,但也会带来额外的时间和空间开销。合理的初始容量和负载因子设置可优化性能。
摘要由CSDN通过智能技术生成

引言

在计算机科学中,HashMap是一种常用的数据结构,用于存储键值对。它提供了高效的插入、查找和删除操作。然而,随着元素的增加,HashMap可能会出现性能下降的问题。为了解决这个问题,HashMap引入了扩容机制,了解扩容机制,可以提高我们对HashMap深层逻辑的理解。

========================================================
关于HashMap中数组与桶、哈希函数的内容可以点击下方链接进行跳转
HashMap详解-内部实现原理(1)-数组和桶
HashMap详解-内部实现原理(2)-哈希函数

HashMap为什么要扩容

主要有以下几点:

  1. 负载因子:
    HashMap使用负载因子(load factor)来衡量哈希表的满载程度。当哈希表中存储的元素数量超过负载因子与初始容量的乘积时,就会触发扩容操作。
    如果负载因子设置得过小,哈希表会很快满载,导致频繁的冲突和查找时间的增加。而设置得过大,则会浪费大量的空间。通过扩容,可以在保持合理负载因子的前提下提供更多的存储空间。

  2. 冲突增加:
    当HashMap中存储的元素增多时,桶中的元素可能发生冲突,即多个键映射到同一个桶上。冲突的增加会降低HashMap的性能,导致插入、查找和删除操作所需的时间复杂度增加。通过扩容,可以减少冲突的概率,提高哈希表的性能。

  3. 提高空间利用率:
    扩容后的新哈希表通常具有更大的容量,这可以减少桶的填充因子,提高空间利用率。较低的填充因子可以减少冲突,使得元素分布更均匀,减少链表长度,从而提高数据访问的效率。

  4. 动态适应需求:
    =当元素数量增加时,通过扩容可以及时提供足够的存储空间,避免发生哈希表溢出或性能下降的问题。

使用一句话概括就是——为了提高效率

扩容(resize)的触发条件

以下是HashMap中resize方法的部分引用:

  final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 
  {
			...
           while (s > threshold && table.length < MAXIMUM_CAPACITY)
                    resize();
			...
    }

```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 
{		
		....
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		....
        if (++size > threshold)
            resize();
		....
    }
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) 
{
		....
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
		....
}
final void treeifyBin(Node<K,V>[] tab, int hash) 
{
        ....
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
		....
    }
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) 
{
		.....
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
		....
}

通过这些对resize的使用我们可以看出来resize通常在什么时候被使用:

  • HashMap实行了懒加载, 新建HashMap时不会对table进行赋值, 而是到第一次插入时, 进行resize时构建table;
  • 当HashMap所存大于threshold(阈值)时, 会进行resize;

PS:阈值(threshold)的简易计算公式为:threshold = table.length * loadFactor;

以下是HashMap中该计算逻辑的部分代码,其中的newCap指的是新的容量

        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;

扩容过程

HashMap扩容的步骤:

  1. 当HashMap中的元素数量达到或超过负载因子(loadFactor)与当前容量(capacity)的乘积时,就会触发扩容操作。

  2. 扩容操作会将HashMap的容量增加一倍,并重新计算新的容量(newCap)。

  3. 创建一个新的数组(newTable)来存储扩容后的元素。

  4. 遍历原始数组(oldTable)中的每个存储桶,将桶中的元素重新分配到新的数组中。重新分配的过程是根据元素的哈希值和新的容量进行重新计算位置。

  5. 如果原始数组的某个存储桶中只有一个元素,则直接将该元素放入新数组的相应位置。

  6. 如果原始数组的某个存储桶中存在多个元素,则会根据元素的哈希值重新计算元素在新数组中的位置。如果新位置上已经有元素存在,则以链表或树形结构的形式处理冲突。

  7. 所有元素都重新分配到新数组后,新的数组将成为HashMap的存储结构,并替代原始数组。

  8. 更新HashMap的容量(capacity)和阈值(threshold),容量更新为新的容量(newCap),阈值更新为新容量乘以负载因子。

通过扩容操作,可以保证HashMap在元素数量增加时仍能保持较低的查找时间复杂度。同时,通过调整负载因子和容量的大小,可以在性能和空间利用率之间达到平衡。
由于每次扩容都是以2的倍数进行计算,因此HashMap的容量一定是2的n次方

扩容的性能影响

HashMap的扩容操作也会对性能产生一定影响。

  • 时间和空间开销:扩容需要重新计算元素的哈希值,并将它们重新分配到新的数组中。这个过程涉及遍历旧数组、计算哈希值、复制元素等操作,可能会消耗较多的时间和空间。

  • 扩容触发频率:扩容操作是在HashMap的负载因子(loadFactor)和元素数量之间比较来判断是否需要进行扩容。如果负载因子设置得过高或元素数量增长较快,就会频繁触发扩容操作,从而对性能产生不利影响。

  • 散列冲突减少:扩容后桶的数量增加,可以使每个桶中存放的元素数量变少,减少散列冲突的发生。这会提高元素的查找、插入和删除效率,从而改善性能。

  • 内存占用增加:扩容后,由于桶的数量增加,HashMap需要占用更多的内存空间来存储元素和相关数据结构。这会增加总体的内存占用,对于内存有限的情况可能会导致性能下降。

  • 迭代性能降低:在进行扩容操作期间,如果有其他线程在对HashMap进行迭代操作,可能会导致遍历过程出现不确定性和错误。这是因为扩容操作涉及元素的重新分配和复制,可能会导致某些元素在不同的阶段被访问或丢失。

为了平衡性能影响,需要合理设置负载因子和初始化容量,以及定期评估和调整HashMap的初始容量。通常情况下,较低的负载因子和适当的初始容量可以减少扩容操作的频率和开销,从而提高HashMap的性能。

扩容过程中的数据迁移

HashMap的扩容过程中涉及数据的迁移,具体步骤如下:

  1. 创建一个新的数组(newTable),其大小是原始数组的两倍(newCap = oldCap * 2)。

  2. 遍历原始数组(oldTable)中的每个存储桶。

  3. 对于每个非空的存储桶,将其中的元素重新计算哈希值,并重新分配到新的数组中的对应位置。

  4. 如果新的数组中的该位置为空,则直接放置元素到该位置。

  5. 如果新的数组中的该位置已经有其他元素存在,可能发生了哈希冲突。这时,根据HashMap的实现方式,可以采用链表或树的形式处理冲突。

  6. 如果链表的长度小于等于8(默认阈值),则将新元素追加到链表的末尾。

  7. 如果链表的长度大于8,则将链表转换为红黑树结构,以提高查找、插入和删除的性能。

  8. 继续遍历原始数组的下一个存储桶,重复步骤3~5,直到遍历完所有的存储桶。

  9. 所有元素都重新分配到新的数组中后,新的数组(newTable)将替代原始数组(oldTable)成为HashMap的存储结构。

需要注意的是,在数据迁移期间,HashMap可能会处于不稳定的状态。因为在扩容过程中,既有的元素可能仍然位于原始数组中,而新的元素已经被分配到新数组中。这可能导致在迭代HashMap时出现未知的行为。

为了避免在迭代期间的并发修改问题,建议在进行数据迁移期间使用一些同步机制或使用线程安全的HashMap实现。或者可以在迁移完成后再进行迭代操作。

补充:HashMap无法保证线程安全, 所以最好使用ConcurrentHashMap

结语

总结HashMap的扩容机制,强调了扩容的重要性和影响。在实际使用HashMap时,合理设置初始容量和负载因子,能够减少扩容的次数,提高HashMap的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值