HashMap 深入揭秘:从入门到大厂必备知识!

B站影视 2024-12-27 10:27 1

摘要:Hi,大家好,我是小米,一个喜欢分享技术的29岁程序员。今天和大家聊聊一个在Java面试中非常经典的问题:“说一下 HashMap 的实现原理?”。别着急,我会用讲故事的方式,把它掰开了揉碎了讲清楚,让你听完之后,再也不怕这个问题!

Hi,大家好,我是小米,一个喜欢分享技术的29岁程序员。今天和大家聊聊一个在Java面试中非常经典的问题:“说一下 HashMap 的实现原理?”。别着急,我会用讲故事的方式,把它掰开了揉碎了讲清楚,让你听完之后,再也不怕这个问题!

一天,产品经理找到你,说用户需要存储一组“键值对”,并且希望能通过“键”快速找到对应的“值”。作为一个有经验的开发者,你第一时间就想到了Java里的HashMap,对吧?

HashMap 就像是一个超级高效的存储工具,它的核心理念是:通过哈希算法定位存储位置,快速存取数据。接下来,我们就一起探讨它的实现原理。

1. 底层数据结构

HashMap 的底层是由一个数组和多个链表/红黑树组成的。这种结构有个专业名词,叫做“拉链法”。我们可以把它想象成一个巨大的仓库,里面有许多货架,每个货架上又挂了很多小篮子:

数组:仓库的货架,负责存储数据的入口;链表/红黑树:每个篮子,存储可能冲突的键值对。

2. 核心字段

HashMap 有几个非常重要的字段,分别是:

容量(capacity):数组的大小,默认是16;负载因子(loadFactor):控制数组什么时候扩容,默认是0.75;阈值(threshold):容量 × 负载因子,当元素个数超过这个值时,就需要扩容。

当我们调用 put(K key, V value) 方法时,HashMap 会经历以下几个步骤:

1. 计算哈希值

通过 key 的 hashCode 方法计算哈希值,并通过一个位运算 h & (n - 1) 确定该数据应该存储到数组的哪个索引上。

为什么不用取模,而用位运算呢?

因为位运算比取模快很多,尤其是在需要高性能的场景下。

2. 定位桶(Bucket)

找到数组的对应位置,查看这个位置上是否已经有数据:

如果没有数据,直接存储;如果已经有数据,则发生了哈希冲突

3. 解决哈希冲突

哈希冲突是不可避免的,HashMap 通过两种方式来解决:

链表:在同一个索引处存储多个键值对;红黑树:当链表的长度超过8时,会转化为红黑树,提升查找效率。

4. 插入数据

将新的键值对插入到对应的链表或红黑树中。如果 key 已经存在,会覆盖旧的 value。

当我们调用 get(K key) 方法时,HashMap 会进行以下操作:

1. 再次计算哈希值

通过 key 的 hashCode 方法计算哈希值,定位到数组的某个索引。

2. 查找桶内数据

进入桶中,依次遍历链表或红黑树:如果找到对应的 key,就返回 value;如果遍历完没有找到,返回 null。

注意:这里的 key 比较是通过 equals 方法完成的。

扩容机制

当 HashMap 的元素数量超过阈值(capacity × loadFactor)时,就会触发扩容:

数组的大小变为原来的两倍;重新计算每个键值对的哈希值,并放入新的数组中。

扩容是个性能开销较大的操作,所以在使用 HashMap 时,我们通常会预估大小,尽量减少扩容次数。

当你讲完上述内容,面试官可能会进一步问你一些细节问题:

1. 为什么数组大小是2的幂次?

因为 h & (n - 1) 的位运算只有在数组大小是2的幂次时,才能均匀分布哈希值,减少冲突。

2. 红黑树是如何提高效率的?

链表的时间复杂度是 O(n),而红黑树的查找复杂度是 O(log n)。当链表太长时(超过8),会自动转化为红黑树,显著提高查找效率。

3. HashMap 是线程安全的吗?

HashMap 不是线程安全的。如果在多线程环境下使用,可以考虑使用 ConcurrentHashMap。

最后,分享几个关于 HashMap 的小技巧:

合理设置初始容量:避免频繁扩容,提升性能;重写 hashCode 和 equals 方法:确保键的哈希值均匀分布,减少冲突;多线程场景用 ConcurrentHashMap:避免线程安全问题。

写到这里,关于 HashMap 的实现原理,我们就聊到这里啦!希望通过这个故事,你对 HashMap 的理解能更上一层楼。如果你觉得这篇文章有帮助,记得点个 或分享给朋友哦!下次再见啦,拜拜~

来源:Sug科技聚焦

相关推荐