HashMap 是 Java 集合框架中非常重要且常用的一部分,下面我将从多个角度为你全面解析它。
核心定义与概述
HashMap 是一个基于 哈希表 实现的、用于存储键值对 的集合,它继承自 AbstractMap 类,并实现了 Map 接口。
核心特点:
- 键值对存储:每个元素都包含一个唯一的键 和一个与之对应的值,通过键可以快速找到对应的值。
- 允许 null 键和 null 值:
HashMap中最多可以有一个null键,但可以有多个null值。 - 无序性:
HashMap不保证元素的存储顺序,你遍历它时,元素的顺序可能与插入顺序不同,也可能每次遍历的顺序都不同(在 Java 8 之前尤其如此,Java 8 引入了TreeNode后,顺序稍微稳定一些,但仍不保证插入顺序)。 - 线程不安全:
HashMap不是线程安全的,如果在多线程环境下对HashMap进行并发修改,可能会导致数据不一致或死循环等问题,如果需要在多线程环境下使用,应该使用ConcurrentHashMap或Hashtable。 - 高效性:在理想情况下,
HashMap的增、删、改、查操作的时间复杂度接近 O(1),这是它最大的优势。
底层实现原理
理解 HashMap 的底层原理是掌握它的关键,其核心是 “数组 + 链表 + 红黑树” 的组合结构,也称为 哈希桶。
核心概念
Node<K,V>:HashMap中的基本数据单元,是一个内部类,代表了存储在哈希表中的一个键值对,它包含hash(键的哈希值)、key、value和next(指向下一个Node的指针)。- 哈希表:一个
Node<K,V>[]类型的数组,我们称之为 “桶数组” (Table),这是HashMap存储数据的核心。 - 哈希函数:将任意
Object(key)转换成一个整数(hash值)的函数,在 Java 中,key.hashCode()方法返回一个初始哈希值,HashMap会对这个值进行二次处理,以减少哈希冲突。 - 哈希冲突:不同的
key经过哈希函数计算后,可能得到相同的数组索引,这是不可避免的,HashMap通过以下两种方式来解决:- 链地址法:在同一个索引位置上,所有冲突的
Node以链表的形式存储。 - 红黑树优化:当链表的长度超过一定阈值(
TREEIFY_THRESHOLD,默认为 8)时,并且桶数组的长度也达到一定要求(MIN_TREEIFY_CAPACITY,默认为 64),该链表会转换成一颗平衡的红黑树,以提高查询效率(从 O(n) 降到 O(log n))。
- 链地址法:在同一个索引位置上,所有冲突的
工作流程
存储元素 (put 方法)
- 计算哈希值:对
key的hashCode()进行扰动处理(异或或位运算),得到一个最终的hash值。 - 计算索引:通过公式
(n - 1) & hash(n是桶数组的长度)计算出该元素应该存放在桶数组中的哪个位置(索引)。 - 处理冲突:
- 桶为空:如果计算出的索引位置上没有元素(
table[i] == null),则直接创建一个新的Node并放入该位置。 - 桶不为空(发生冲突):
key已经存在(通过equals方法比较),则用新值覆盖旧值,并返回旧值。key不存在,则将新的Node插入到链表或红黑树中。- 如果是链表,则采用 头插法(Java 8 之前)或 尾插法(Java 8 之后)将新节点加入。
- 如果链表长度超过 8,并且桶数组长度 >= 64,则将链表转换为红黑树。
- 桶为空:如果计算出的索引位置上没有元素(
- 扩容:当
HashMap中的元素数量(size)超过 *容量 加载因子* (`capacity loadFactor) 时,HashMap` 会进行扩容,扩容会创建一个新的、长度是原来两倍的桶数组,然后将所有旧元素重新计算索引并迁移到新数组中,这是一个非常耗时的操作。
获取元素 (get 方法)
- 计算哈希值和索引:与
put方法一样,先计算key的hash值,再计算出它在桶数组中的索引位置。 - 查找元素:
- 如果该位置为空,返回
null。 - 如果不为空,比较
key是否相等(先比较hash,再比较equals)。 - 如果是链表,则遍历链表查找。
- 如果是红黑树,则在树中查找。
- 如果该位置为空,返回
- 返回找到的
value,如果没找到则返回null。
常用构造方法
HashMap 提供了几个常用的构造方法,让你可以自定义其容量和加载因子。
-
HashMap()- 描述:创建一个空的
HashMap,默认初始容量为 16,默认加载因子为 75。 - 示例:
HashMap<String, Integer> map = new HashMap<>();
- 描述:创建一个空的
-
HashMap(int initialCapacity)- 描述:创建一个具有指定初始容量的
HashMap,加载因子仍为默认的 0.75。 - 注意:容量会被自动调整为大于等于
initialCapacity的最小的 2 的幂次方,你传入 10,实际容量会是 16。 - 示例:
HashMap<String, Integer> map = new HashMap>(32); // 初始容量为 32
- 描述:创建一个具有指定初始容量的
-
HashMap(int initialCapacity, float loadFactor)- 描述:创建一个具有指定初始容量和加载因子的
HashMap。 - 加载因子:它是一个衡量
HashMap满程度的指标,默认 0.75 意味着当HashMap中元素数量达到容量的 75% 时,就会触发扩容,加载因子越大,空间利用率越高,但哈希冲突的概率也越大,查询效率会降低;反之,加载因子越小,冲突概率低,但空间浪费多。 - 示例:
HashMap<String, Integer> map = new HashMap>(64, 0.5f); // 容量64,加载因子0.5
- 描述:创建一个具有指定初始容量和加载因子的
常用方法
| 方法 | 描述 | 示例 |
|---|---|---|
V put(K key, V value) |
将键值对存入 Map。key 已存在,则覆盖旧值并返回旧值;否则返回 null。 |
map.put("apple", 10); |
V get(Object key) |
根据键获取对应的值。key 不存在,返回 null。 |
Integer price = map.get("apple"); |
V remove(Object key) |
移除指定 key 的键值对,并返回被移除的值。key 不存在,返回 null。 |
map.remove("apple"); |
boolean containsKey(Object key) |
判断 Map 中是否包含指定的 key。 |
boolean hasApple = map.containsKey("apple"); |
boolean containsValue(Object value) |
判断 Map 中是否包含指定的 value。 |
boolean hasPrice10 = map.containsValue(10); |
int size() |
返回 Map 中键值对的数量。 |
int count = map.size(); |
boolean isEmpty() |
判断 Map 是否为空。 |
boolean empty = map.isEmpty(); |
void clear() |
清空 Map 中的所有元素。 |
map.clear(); |
Set<K> keySet() |
返回包含所有 key 的 Set 集合。 |
Set<String> keys = map.keySet(); |
Collection<V> values() |
返回包含所有 value 的 Collection 集合。 |
Collection<Integer> prices = map.values(); |
Set<Map.Entry<K,V>> entrySet() |
返回包含所有键值对(Map.Entry)的 Set 集合,常用于遍历。 |
for (Map.Entry<String, Integer> entry : map.entrySet()) { ... } |
遍历方式
遍历 HashMap 有多种方式,推荐使用 entrySet(),因为它效率最高。
使用 for-each 循环遍历 entrySet() (推荐)
HashMap<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
map.put("Three", 3);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
使用 forEach 方法 (Java 8+)
map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
遍历 keySet()
for (String key : map.keySet()) {
System.out.println("Key: " + key + ", Value: " + map.get(key));
}
使用迭代器遍历 entrySet()
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
与 Hashtable 的区别
| 特性 | HashMap |
Hashtable |
|---|---|---|
| 线程安全 | 不安全 | 安全 (方法均 synchronized) |
| 性能 | 高 | 低 (因为有同步开销) |
| Null 键/值 | 允许一个 null 键和多个 null 值 |
不允许 null 键或 null 值 |
| 父类 | AbstractMap |
Dictionary (较老的类) |
| 迭代器 | 快速失败 | 快速失败 |
HashMap 是 Java 开发中不可或缺的工具,它通过哈希表实现了高效的键值对存储。
- 何时使用:当你需要根据一个唯一的键来快速查找、插入或删除数据时,
HashMap是首选。 - 核心要点:理解其 数组+链表+红黑树 的结构、哈希冲突 的解决方式以及 扩容机制,能帮助你更好地使用它并排查问题。
- 线程安全:牢记它是非线程安全的,在多线程环境下应使用
ConcurrentHashMap。
