HashMap 之底层数据结构和扩容机制

简介: HashMap 之底层数据结构和扩容机制

一、底层实现原理

1.1 底层数据结构

HashMap 的底层数据结构是一个由数组和链表实现的类似字典的结构,或者是红黑树的结构。在 HashMap 的长度小于 8 时是前者,大于或等于 8 时是后者。这也就意味着,HashMap 不仅仅会扩容,还可以“缩容”。

1.1.1 在 JDK1.8 之前

在这之前,HashMap 还没有被优化,底层的数据结构只可能是 数组 + 链表 的方式,不存在 HashMap 长度大于等于 8 后数据结构转变为红黑树的情况。那么,是怎么个 数组 + 链表的结构呢?如下图:

数组 + 链表

通过上面的图,我们可以很清晰地看懂 HashMap 的 数组 + 链表 结构,其中,数组中的元素是每个链表的头节点。

1.1.2 在 JDK1.8 及 1.8 之后

JDK1.8 之后,HashMap 被极大地优化了,在长度小于 8 时没有太大变化,但跟以往不同,在存储的数据特别大的情况下(长度至少大于等于 8),HashMap 的性能有了明显的改善,这要得益于数据结构转变成了红黑树。关于数据结构 —— 红黑树,我们这里不做详细的说明,具体参见:【红黑树】。

红黑树具有良好的性能,加速了 HashMap 一些操作的速度。此时 HashMap 的底层数据结构大致像下面这样:

数组 + 链表 + 红黑树

1.2 put 方法

put 是 HashMap 的一个方法,它可以往 HashMap 中添加或者修改一个键值对,它的具体过程如下:

在不考虑 HashMap 的长度达到 8 时结构由链表转变为红黑树的这种情况下,put 一个键值对 <key, value>,首先 key 和 value 会首先被实例化为一个实现了 Map.Entry<key, value> 接口的 Node 对象,然后调用 key 的 hashCode 方法得到 key 的哈希值,并与 HashMap 中已有的键的哈希值进行比较,如果在 HashMap 中没有出现这个哈希值,那么这个 Node 对象就会被视为一个新的键值对,并被添加到 HashMap 中去,如果出现了相同的哈希值,则会再次调用 key 的 equals 方法对 HashMap 中原有的这个键进行比较,若不同,则说明此时发生了哈希冲突,并将这个键值对添加到 HashMap 中去,若没有出现上述过程,则说明 HashMap 中已经有这个键值对了,那么就用 value 替换对应 key 的值。

下面用一张图展示了这个过程:

put 方法执行过程

当然,若是添加之后,HashMap 的长度大于或等于 8,那么在添加的时候还有将 HashMap 底层数据由链表转换为红黑树这一过程。这一部分在后续的“扩容机制”一节中将具体讲解。

二、扩容机制

2.1 负载因子(loadFactor)

HashMap 的负载因子是一个用来计算扩容阈值的数,计算公式是:

阈值(threshold)= 负载因子(loadFactor)× 容量(capacity)

当 HashMap 存储的数据个数达到阈值时,HashMap 将会自动进行容量扩充,负载因子的值默认为 0.75。

那么,为什么要有负载因子的存在呢?像 ArrayList 那样,当容量不够的时候再进行扩容难道不行吗?实际上,不是不行,只是不好。由于 HashMap 的增删改查或多或少涉及到哈希相关算法,若是像 ArrayList 那样,容量不够的时候再进行扩容,很容易导致出现“哈希碰撞”的情况,而出现“哈希碰撞”时就需要调用 equals 方法来解决这一问题,而 equals 方法的性能并不高,为了提高 HashMap 的性能,就需要适当地提前进行扩容。但是,又不能扩容非常多,因为扩容太多,而数据存储量并没有那么多的话,就会耗费过多内存空间,于是就有了负载因子的存在。

HashMap 的扩容本质上就是一个对时间和空间权衡的问题,想要时间少,那么空间就要耗费多,反之,想要空间耗费小,那么时间就要消耗多。只有一个不大不小的值,才能权衡这二者之间的利弊。

但是,这个值为 0.75 是怎么计算出来的呢?

首先,负载因子不能太大,也不能太小,我们可以大致估计出它在 0.5 ~ 1 之间最为合适,其中间值 0.75 是个不错的选择。其次,我们要知道,HashMap 每次扩容与 ArrayList 扩容 1.5 倍不同,HashMap 每次直接翻倍,而 HashMap 初始化后的默认容量是 16(24),因此 HashMap 的容量一般来说都是 2 的指数幂,而 0.75(3/4)与任何大于等于 4 的 2 指数次幂相乘的结果都是整数,正好,HashMap 的默认容量 16 是满足这个条件的,这样就避免了分数的麻烦。

2.2 默认容量

HashMap 初始化后的默认容量是 16,那么为什么是 16,而不像 ArrayList 那样,初始为 0,要添加的时候再进行扩容呢?

实际上这个值也是通过对时间和空间的权衡利弊得到的。我们可以简单分析一下,根据上面负载因子的值及扩容相关知识,我们可以知道,初始容量应该为 2 的指数次幂才可以保证 HashMap 的容量始终是 2 的指数次幂。很显然,当默认容量取得太大时,会有很多空间不一定利用,导致了空间的浪费,而当默认容量太小时,稍微让 HashMap 存储多一点的数据,它就需要多次调用扩容相关的代码,一次只扩容一倍,非常耗时。通过权衡时间和空间的利弊,HashMap 的默认容量就被定为了 16。

2.3 缩容

一般情况下,是不建议对 HashMap 进行缩容的,但是,这并不代表 HashMap 无法进行缩容了,它只是不会自动地缩容,要想缩容,必要手动进行。

2.3.1 不缩容的理由

我们不妨想想,缩容是通过时间来空间的操作,如果是在空间充足的情况下,这只会消耗时间,就算是空间不太充足的情况下,既然你扩容到了这么大,那么说明你确实有这么多的数据要存储,那又为什么要缩容呢?所以,一般来说,是没必要缩容的,除非真的在某一刻程序急需内存来处理某些棘手的问题,这个时候才需要缩容来腾出空间给其他程序使用,但这样对于空间不足的问题治标不治本,更好的方法是升级硬件配置或者改善程序来增加可用内存空间。

2.3.2 如何缩容

很简单的一种方法,实例化一个新的 HashMap,然后把原来 HashMap 的数据搬过来,再释放原来 HashMap 的内存空间,以达到“缩容”的目的。但是,这显然不是真正的缩容,实际上,Java 是不支持直接对一个 HashMap 对象进行真正意义上缩容的,从之前“不缩容的理由”一节中就可以看出,这样做,真的不是一个好的做法。

总结一句话就是,HashMap 缩容,没有必要。

目录
相关文章
|
6天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
34433 17
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
18天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
45261 142
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
8天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4779 20
|
1天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
1264 5
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
6天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1680 5
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案

热门文章

最新文章