HashMap
是 Java 中用于存储键值对的集合类,提供了高效的插入、删除和查找操作。它的底层数据结构是哈希表。以下是 HashMap
的一些关键点和底层原理:
1. 底层数据结构
HashMap
的底层实现是一个数组加上链表(或红黑树)。每个数组位置称为一个桶(bucket),用于存储哈希冲突的元素。
- 数组: 用于存储桶的引用。数组的大小会根据
HashMap
的容量自动调整。 - 链表: 用于处理哈希冲突,即不同键的哈希值相同或者碰撞的情况。这些键值对会以链表的形式存储在同一个桶中。
- 红黑树: 从 Java 8 开始,当一个桶内的链表长度超过一定阈值(默认为 8)时,链表会转化为红黑树,以提高查找性能。红黑树是一种自平衡的二叉搜索树。
2. 哈希函数和哈希码
HashMap
使用键的哈希码(通过 hashCode
方法)来确定键值对存储的桶位置。哈希函数将键的哈希码映射到数组的索引位置。
- 哈希码: 每个对象都有一个
hashCode
方法,返回对象的哈希码。 - 哈希函数: 通过对哈希码进行扰动运算(如异或、位移等),减少哈希冲突的概率,并确保更均匀的分布。
3. 哈希冲突处理
- 链表: 当多个键的哈希码映射到同一个桶时,这些键值对会以链表的形式存储在该桶中。插入新元素时,
HashMap
会检查链表中是否已经存在相同的键。 - 红黑树: 如果链表的长度超过一定阈值,链表会被转换成红黑树,以提高查找效率。红黑树的操作时间复杂度是 O(log n),而链表是 O(n)。
4. 负载因子和扩容
- 负载因子: 决定
HashMap
的填充程度。默认负载因子是 0.75,这意味着哈希表会在填充达到 75% 时进行扩容。 - 扩容: 当
HashMap
的实际大小超过负载因子与当前容量的乘积时,HashMap
会进行扩容。扩容会创建一个新的、更大的数组,并将现有的键值对重新哈希到新的数组中。扩容的时间复杂度是 O(n),因为需要重新计算每个元素的位置。
5. 常见操作的时间复杂度
- 插入(put): 平均时间复杂度是 O(1)。由于哈希表的操作大部分是常数时间复杂度,但在最坏情况下(例如哈希冲突过多)可能会退化到 O(n)。
- 查找(get): 平均时间复杂度是 O(1),与插入操作类似。
- 删除(remove): 平均时间复杂度是 O(1),与插入操作类似。
示例代码
以下是一个简单的 HashMap
示例代码:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// 输出映射
System.out.println("HashMap: " + map);
// 查找值
System.out.println("Value for 'banana': " + map.get("banana"));
// 删除键值对
map.remove("banana");
System.out.println("HashMap after removal: " + map);
// 遍历键值对
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 集合的大小
System.out.println("Size of HashMap: " + map.size());
}
}
总结
HashMap
的核心在于通过哈希表来存储键值对,提供了高效的插入、查找和删除操作。通过使用链表和红黑树来处理哈希冲突,HashMap
在大多数情况下能保持良好的性能。