1. HashMap 的底层数据结构
HashMap
是 Java 中实现了 Map
接口的一个常用类,主要用来存储键值对(Key-Value)。它底层依赖于 哈希表(Hash Table)实现,主要使用 数组 和 链表(或 红黑树)两种数据结构。
主要组成:
- 数组:
HashMap
使用一个数组来存储所有的桶(bucket),每个桶可以存储一个或多个键值对。 - 链表:当多个键值对的哈希值相同时(即哈希冲突),这些元素会存储在同一个桶的位置,通过链表来组织。
- 红黑树:当链表的长度超过一定阈值(默认为 8),链表会转换为红黑树,以提高查找效率。
数据结构演变:
- JDK 1.7 及之前版本:哈希冲突通过链表解决。
- JDK 1.8 及以后版本:链表长度超过阈值时,链表会转为红黑树。
2. put
操作的执行流程
put
方法用于将一个键值对插入 HashMap
中,具体过程如下:
(1) 计算哈希值:
- 调用
key.hashCode()
计算键的哈希值。为了减少哈希冲突,哈希值会进行扰动处理(位运算)。
int hash = key.hashCode();
(2) 计算数组索引位置:
- 通过
hash
与数组长度(n
)的位运算来确定该元素应该存储在数组的哪个桶位置。该操作可以帮助降低哈希冲突。
int index = (n - 1) & hash; // 计算桶的索引位置
(3) 检查该位置是否为空:
- 如果目标桶位置为空,则直接插入该键值对。
- 如果桶位置已经有元素(发生哈希冲突),则需要进行冲突解决。
(4) 处理哈希冲突:
- 链表法:如果目标位置有元素(发生哈希冲突),则会通过链表(或红黑树)进行处理。每个链表节点存储一个键值对,发生冲突时,新的键值对会被添加到链表末尾。
- 红黑树法:当链表长度超过阈值(默认 8)时,链表会转为红黑树,以提高查找效率。
(5) 扩容判断:
- 如果
HashMap
的元素数量超过负载因子(默认为 0.75)与当前容量的乘积时,HashMap
会进行扩容操作,将数组的容量加倍,并重新分配桶的位置。
if (++size > threshold) {
resize(); // 扩容
}
3. put
操作流程图示
假设我们要插入两个元素:
put("key1", "value1");
put("key2", "value2");
计算哈希值并通过哈希值计算桶的位置:
hash(key1)
->index0
hash(key2)
->index1
将元素插入相应的位置:
[桶0] -> (key1, value1)
[桶1] -> (key2, value2)
- 如果发生冲突,则会将新元素插入到链表的末尾。
[桶0] -> (key1, value1)
[桶1] -> (key2, value2)
[哈希冲突] -> 链表/红黑树
4. 性能分析
- 平均时间复杂度:
put
操作的平均时间复杂度是 O(1),即常数时间。前提是哈希表负载均衡,哈希冲突较少。 - 最坏时间复杂度:最坏情况下,如果所有元素都被存储在同一个桶中(发生严重冲突),则时间复杂度是 O(n),其中 n 是
HashMap
中的元素数量。但这种情况通常较少发生,通过良好的哈希函数和扩容机制可以避免。
5. 扩容机制
HashMap
会在元素数量超过负载因子(默认是 0.75)与当前容量的乘积时进行扩容。扩容是将数组的大小翻倍,并将现有元素重新映射到新的数组位置。扩容过程会消耗较多时间,因此,初始化时尽量合理配置 HashMap
的初始容量和负载因子,以避免频繁扩容。
if (++size > threshold) {
resize(); // 扩容
}
6. 总结
HashMap
的底层是基于数组和链表(或红黑树)来存储元素的。put
操作的主要步骤包括计算哈希值、确定桶的位置、处理哈希冲突、扩容等。HashMap
提供高效的存储和查询操作,在大多数情况下,put
的时间复杂度为 O(1)。- 在极端情况下,哈希冲突较多时,最坏时间复杂度可能是 O(n),但通过合理的哈希函数和扩容机制可以降低冲突的发生概率。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。