深入解析Java HashMap的Resize源码

2024-06-05 10:20

本文主要是介绍深入解析Java HashMap的Resize源码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java中的HashMap是一个常用的数据结构,底层实现由数组和链表(或红黑树)组成。随着插入元素的增多,HashMap需要扩容以维持高效的性能。本文将深入解析HashMap的扩容机制——resize()方法,通过逐行代码解释其实现原理和背后的设计思想。

1. HashMap的基本结构

在深入resize()方法的分析之前,首先理解HashMap的基本结构和工作机制。HashMap主要由以下几个部分组成:

  1. 数组(table):存储键值对节点的主要结构。
  2. 链表:在哈希冲突时,多个键值对存储在数组的同一个位置,以链表形式连接在一起。
  3. 红黑树:在Java 8之后,当链表长度超过一定阈值(默认是8)时,链表会转换成红黑树以提高查询效率。

2. HashMap扩容的必要性

随着HashMap中的元素增多,负载因子(元素数量/数组容量)接近或超过默认值(0.75)时,查询和插入效率会显著下降。为了保持高效操作,HashMap在元素数目超过一定阈值时进行扩容。扩容的核心是resize()方法。

3. resize()方法源码分析

以下是resize()方法的源码:

final HashMap.Node<K, V>[] resize() {HashMap.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)newCap = oldThr;else {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"})HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {HashMap.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)((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);else {HashMap.Node<K, V> loHead = null, loTail = null;HashMap.Node<K, V> hiHead = null, hiTail = null;HashMap.Node<K, V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;} else {if (hiTail == null)hiHead = e;elsehiTail.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;
}

3.1 计算新容量和新阈值

resize()方法的开头,首先计算新数组的容量和新的阈值。通过检查旧数组的容量和阈值,方法决定新的容量和阈值:

HashMap.Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;

接着根据旧的容量和阈值,分几种情况处理:

3.1.1 旧容量大于0

如果旧数组的容量大于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;
}
  • 若旧容量大于等于最大容量,设置阈值为最大整数值,不再扩容。
  • 若旧容量小于最大容量,且旧容量大于等于默认初始容量,则新容量为旧容量的两倍,新的阈值为旧阈值的两倍。
3.1.2 旧阈值大于0

如果旧数组的容量为0,但旧阈值大于0,说明是在初始化时指定了初始容量:

else if (oldThr > 0)newCap = oldThr;

将新容量设置为旧阈值。

3.1.3 初始默认值

如果旧数组容量和阈值都为0,使用默认值进行初始化:

else {newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

3.2 计算新阈值

如果新的阈值未被设置,则根据新的容量和加载因子计算新的阈值:

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

3.3 创建新数组

创建一个新的数组newTab,并将其赋值给table

@SuppressWarnings({"rawtypes", "unchecked"})
HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];
table = newTab;

3.4 元素迁移

将旧数组中的元素重新散列到新数组中:

if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {HashMap.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)((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);else {HashMap.Node<K, V> loHead = null, loTail = null;HashMap.Node<K, V> hiHead = null, hiTail = null;HashMap.Node<K, V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;} else {if (hiTail == null)hiHead = e;elsehiTail.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的分布均匀性和查询效率。

3.5 拆分链表

在迁移过程中,如果遇到链表或树节点,需要分别处理:

  • 链表:拆分成两个链表,一个放在低位索引,另一个放在高位索引。
  • 树节点:调用TreeNodesplit方法进行处理。
else if (e instanceofTreeNode)((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {HashMap.Node<K, V> loHead = null, loTail = null;HashMap.Node<K, V> hiHead = null, hiTail = null;HashMap.Node<K, V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;} else {if (hiTail == null)hiHead = e;elsehiTail.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;}
}

3.6 重新分布元素

在处理元素迁移时,通过计算新索引,将元素放置在新数组的适当位置:

  • 对于单个元素,直接根据新容量计算新的索引位置。
  • 对于链表,拆分成两个子链表,分别放置在低位索引和高位索引。
  • 对于红黑树,调用TreeNodesplit方法,按照新容量重新组织树结构。

4. 深度分析与优化

4.1 扩容策略

HashMap的扩容策略是当元素数量超过阈值时,将数组容量翻倍。这种策略有效地减少了哈希冲突,提高了查找效率。阈值的更新逻辑也确保了HashMap在扩容后的负载因子保持在合理范围内。

4.2 重新散列

重新散列(rehash)是扩容过程中最重要的步骤。通过对旧数组中的每个元素重新计算哈希值,并将其放置到新数组中的适当位置,确保了数据的均匀分布。重新散列的计算通过e.hash & (newCap - 1)进行,利用了哈希值的低位特性,使得散列结果更加均匀。

4.3 树化和退化

在迁移过程中,HashMap还考虑了链表的长度。如果链表长度超过阈值(8),链表会转换成红黑树,以提高查找效率;如果链表长度减少到6以下,红黑树会退化成链表。这种设计确保了HashMap在不同负载情况下都能保持高效。

4.4 内存管理

扩容过程中,新旧数组的内存管理也是关键。通过重新分配新数组,并将旧数组的元素迁移到新数组,HashMap在扩容后仍能保持高效的内存使用。

5. 性能优化建议

5.1 优化哈希函数

HashMap依赖哈希函数将键散列到数组的不同位置。优化哈希函数可以减少哈希冲突,提高查找效率。确保哈希函数生成的哈希值均匀分布在整个数组范围内,是优化HashMap性能的关键。

5.2 动态调整阈值

在实际应用中,不同的使用场景可能需要不同的负载因子。通过动态调整阈值,可以在不同负载下优化HashMap的性能。例如,在高并发环境下,可以适当降低负载因子,以减少扩容频率。

5.3 并发扩容

在多线程环境下,HashMap的扩容可能会导致性能瓶颈。引入并发扩容机制,例如分段锁或CAS操作,可以提高HashMap在高并发场景下的性能。Java的ConcurrentHashMap就是通过分段锁机制实现了高并发下的高效扩容。

6. 总结

通过对HashMap的resize()方法的详细分析,可以看出其设计的精妙之处。在扩容过程中,既考虑了性能优化,又保证了数据的正确性。整个过程分为计算新容量和阈值、创建新数组、迁移旧元素三个主要步骤。每一步都精确地考虑了各种可能的情况,使得HashMap在面对不同负载和容量需求时能够高效运作。

HashMap作为Java中重要的数据结构,其内部实现充分展示了数据结构与算法的巧妙结合。理解其扩容机制,对于实际应用中优化性能、合理使用内存具有重要意义。通过不断优化哈希函数、动态调整阈值和引入并发扩容机制,可以进一步提升HashMap的性能,使其在各种复杂应用场景中表现出色。

这篇关于深入解析Java HashMap的Resize源码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1032786

相关文章

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法

《springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法》:本文主要介绍springboot整合阿里云百炼DeepSeek实现sse流式打印,本文给大家介绍的非常详细,对大... 目录1.开通阿里云百炼,获取到key2.新建SpringBoot项目3.工具类4.启动类5.测试类6.测

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

C#中async await异步关键字用法和异步的底层原理全解析

《C#中asyncawait异步关键字用法和异步的底层原理全解析》:本文主要介绍C#中asyncawait异步关键字用法和异步的底层原理全解析,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结C#异步编程

在Spring Boot中浅尝内存泄漏的实战记录

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录使用静态集合持有对象引用,阻止GC回收关键点:可执行代码:验证:1,运行程序(启动时添加JVM参数限制堆大小):2,访问 htt

SpringBoot集成Milvus实现数据增删改查功能

《SpringBoot集成Milvus实现数据增删改查功能》milvus支持的语言比较多,支持python,Java,Go,node等开发语言,本文主要介绍如何使用Java语言,采用springboo... 目录1、Milvus基本概念2、添加maven依赖3、配置yml文件4、创建MilvusClient

浅析Java中如何优雅地处理null值

《浅析Java中如何优雅地处理null值》这篇文章主要为大家详细介绍了如何结合Lambda表达式和Optional,让Java更优雅地处理null值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录场景 1:不为 null 则执行场景 2:不为 null 则返回,为 null 则返回特定值或抛出异常场景

SpringMVC获取请求参数的方法

《SpringMVC获取请求参数的方法》:本文主要介绍SpringMVC获取请求参数的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下... 目录1、通过ServletAPI获取2、通过控制器方法的形参获取请求参数3、@RequestParam4、@

SpringBoot应用中出现的Full GC问题的场景与解决

《SpringBoot应用中出现的FullGC问题的场景与解决》这篇文章主要为大家详细介绍了SpringBoot应用中出现的FullGC问题的场景与解决方法,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录Full GC的原理与触发条件原理触发条件对Spring Boot应用的影响示例代码优化建议结论F