Java中的HashMap底层实现原理
描述:
HashMap是Java集合框架中最常用的数据结构之一,用于存储键值对(key-value)映射。理解其底层实现原理,对于高效使用和应对面试都至关重要。它的核心在于如何通过键(key)来快速定位到对应的值(value)。
解题/讲解过程:
我们将从基础结构开始,逐步深入到核心方法。
第一步:基本结构 - 数组 + 链表/红黑树
你可以把HashMap想象成一个有很多抽屉的柜子(这个柜子就是“数组”)。
- 数组(柜子):每个位置被称为一个“桶”(bucket)。数组的初始长度是固定的(默认16),这是HashMap能够快速定位的基础。
- 链表/红黑树(抽屉里的收纳格):当你把东西放进抽屉时,如果这个抽屉只放一件东西,直接找到。但如果有多件东西,就需要在抽屉里再做一些分隔。在Java 8之前,这个“分隔”使用的是“链表”。但在Java 8及以后,当链表长度过长(超过8)时,为了提升查询效率,它会自动转换成一个更高效的数据结构——“红黑树”。
所以,HashMap的底层就是一个数组,数组的每个元素可能是一个链表的头节点,也可能是一棵红黑树的根节点。
第二步:关键过程 - 如何存入数据(put方法)
假设我们执行 map.put("apple", 10)。这个过程分为以下几个核心步骤:
-
计算索引(决定放入哪个抽屉):
- 首先,HashMap会调用键
"apple"的hashCode()方法,得到一个整型的哈希值。这个值可能非常大(比如123456789)。 - 然后,HashMap并不是直接用这个巨大的哈希值作为数组下标,而是通过一个“扰动函数”对这个哈希值进行二次计算。扰动函数的目的是为了让哈希值的每一位都能参与到后续的运算中,减少“哈希冲突”(不同的key算出了相同的数组下标)。
- 最后,用扰动计算后的哈希值,与(数组长度 - 1)进行一个
&(与)操作,从而得到最终要存放的数组下标。 - 公式简化理解:
index = HashCode(key) & (ArrayLength - 1) - 假设我们计算出的索引是 2,那么键值对就会尝试放在数组的第2个位置上。
- 首先,HashMap会调用键
-
处理冲突(解决抽屉里放多件东西的问题):
- 情况一:当前位置为空。这是最简单的情况,直接创建一个节点(包含key, value, hash值等)放入这个位置即可。
- 情况二:当前位置不为空(发生哈希冲突)。这时需要比较:
- a. 首先,比较新key的哈希值和已存在节点key的哈希值是否相等。
- b. 如果哈希值相等,再比较两个key是否“相等”(使用
equals()方法)。 - 如果都相等:说明是同一个key,则用新的value覆盖旧的value。
- 如果key不相等:说明是不同的key,但计算出了相同的索引,这就是真正的“哈希冲突”。这时,就会采用“链地址法”,将新的键值对作为一个节点,添加到当前桶的链表末尾(或红黑树中)。
第三步:关键过程 - 如何获取数据(get方法)
map.get("apple") 的过程是 put 的逆过程,思路一致:
- 计算key
"apple"的哈希值,通过同样的扰动函数和&操作,定位到数组的索引(比如还是2)。 - 检查该索引位置的桶:
- 如果为空,返回
null。 - 如果不为空,则遍历该桶下的链表或红黑树。先比较哈希值,如果哈希值相同,再用
equals()方法比较key。找到相等的key,就返回其对应的value。
- 如果为空,返回
第四步:动态扩容 - 当柜子满了怎么办
HashMap不是固定大小的。当存储的键值对数量超过一个阈值(阈值 = 数组容量 × 负载因子,默认负载因子是0.75)时,HashMap会进行“扩容”(resize)来减少哈希冲突,保证效率。
- 触发条件:当
size > capacity * loadFactor时(例如默认16*0.75=12),就会触发扩容。 - 扩容操作:
- 创建一个新的数组,大小是原数组的两倍(默认16 -> 32)。
- 遍历旧数组中的每一个桶和桶中的每一个节点,重新计算每个节点在新数组中的索引位置。因为数组长度变了,
index = HashCode(key) & (NewArrayLength - 1)的结果很可能和原来不一样。 - 将所有节点“迁移”到新数组的正确位置上。这是一个比较耗性能的操作。
第五步:Java 8的优化 - 红黑树
在Java 8之前,HashMap在发生严重哈希冲突时,链表会变得非常长,导致查询效率从O(1)退化为O(n)。Java 8引入了红黑树来解决这个问题:
- 转化条件:当同一个桶中的节点数量超过一定阈值(TREEIFY_THRESHOLD,默认为8)并且当前数组的长度达到一定最小值(MIN_TREEIFY_CAPACITY,默认为64)时,链表会转换为红黑树。
- 退化条件:当由于扩容或删除操作,导致红黑树中的节点数量过少(小于UNTREEIFY_THRESHOLD,默认为6)时,红黑树会退化为链表。
- 目的:将最坏情况下的查询时间复杂度从O(n)提升到O(log n),大大提高了性能。
总结:
HashMap的核心原理是一个“数组+链表/红黑树”的结构。它通过key的哈希值快速定位到数组索引。通过解决哈希冲突的链地址法来存储数据,并通过动态扩容和引入红黑树来保证在大多数情况下都具有高效的查询和插入性能(近似O(1))。理解 hashCode() 和 equals() 方法在其中的作用(决定索引位置和判断key是否相等)是掌握HashMap的关键。