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 缩容,没有必要。

目录
相关文章
|
2月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
50 1
|
2月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
75 2
|
27天前
|
存储 Java Serverless
HashMap的底层数据结构是怎样的
在Java中,HashMap是一种基于哈希表的Map接口实现,以其高效的数据存取能力而广泛使用。本文将深入探讨HashMap的底层数据结构,揭示其如何通过数组、链表和红黑树的结合来优化性能。
|
27天前
|
存储 Java Serverless
深入探索:HashMap的底层数据结构揭秘
HashMap作为Java中一个非常重要的集合类,其底层数据结构的设计对于性能有着至关重要的影响。本文将详细解析HashMap的底层数据结构,帮助开发者更好地理解和使用这一强大的工具。
30 7
|
27天前
|
存储 Java Serverless
HashMap的底层数据结构
HashMap作为Java中一个核心的数据结构,以其高效的键值对存储和检索能力而被广泛使用。本文将深入探讨HashMap的底层数据结构,揭示其如何通过精巧的设计实现快速的数据访问。
27 6
|
26天前
|
存储 Java
HashMap的扩容机制是怎样的
在Java中,HashMap 是一个基于哈希表的键值对集合,它以其高效的存取性能而广泛使用。HashMap 的扩容机制是其性能优化的关键部分,本文将详细介绍这一机制的工作原理和过程。
|
26天前
|
存储 Java
HashMap的底层数据结构详解
在Java中,HashMap 是一个非常重要的集合类,用于存储键值对(Key-Value)。它提供了快速的数据插入、删除和查找功能。本文将深入探讨 HashMap 的底层数据结构,帮助读者更好地理解其工作原理。
|
2月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
68 5
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
63 5
|
2月前
|
存储
HashMap扩容机制
【10月更文挑战第11天】 `HashMap`的扩容机制是其重要特性之一。当容量达到负载因子(默认0.75)时,会触发扩容。扩容时,新容量通常是原容量的两倍,元素需重新哈希并迁移到新数组中。此过程涉及大量计算和迁移,可能影响性能。合理设置初始容量和负载因子,可减少不必要的扩容。在多线程环境中,还需注意线程安全问题。