在 Java 键值对(Key-Value)集合中,HashMap 是使用频率最高的实现类之一,凭借高效的查找、插入性能,成为日常开发的 “利器”。本文将从 HashMap 的底层原理、核心特点、常用方法到遍历方式、使用注意事项,进行系统性梳理,帮助快速掌握其核心逻辑与实战技巧。
一、HashMap 核心认知:底层原理与特点
HashMap 本质是哈希表(数组 + 链表 / 红黑树) 实现的键值对存储结构,核心目标是通过 “哈希算法” 快速定位元素,平衡查询与增删效率。其核心特点如下:
1. 底层存储结构(JDK 1.8+)
- 基础结构:数组(称为 “哈希桶”)+ 链表 + 红黑树。
- 数组:每个元素是一个 “链表头节点”,通过
key的哈希值计算数组索引(index = (数组长度 - 1) & 哈希值),实现快速定位。 - 链表:当多个
key计算出相同索引(哈希冲突)时,元素以链表形式存储在对应桶中。 - 红黑树:当链表长度超过 8 且数组长度 ≥ 64 时,链表会转为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n)(避免链表过长导致性能下降)。
- 数组:每个元素是一个 “链表头节点”,通过
- 示意图简化理解:
数组索引 0:[Node(key1, val1)] → [Node(key2, val2)] // 链表(长度<8)
数组索引 1:[TreeNode(key3, val3)] → 红黑树结构 // 红黑树(长度≥8)
数组索引 2:null
...
2. 核心特点
- 键值对规则:
- Key 唯一:若添加重复 Key,新 Value 会覆盖旧 Value(
put()方法返回旧 Value)。 - Value 可重复:不同 Key 可对应相同 Value。
- 允许 null:Key 最多允许 1 个 null(重复添加 null Key 会覆盖),Value 可多个 null。
- Key 唯一:若添加重复 Key,新 Value 会覆盖旧 Value(
- 无序性:存储顺序与插入顺序无关(底层按哈希值排序,非插入顺序)。
- 线程不安全:非同步设计,多线程同时读写可能出现数据异常(如
ConcurrentModificationException),需手动处理线程安全(如Collections.synchronizedMap()或ConcurrentHashMap)。 - 自动扩容:
- 默认初始容量:16(数组长度,必须是 2 的幂,确保哈希计算均匀)。
- 负载因子:默认 0.75(当元素数量 ≥ 容量 × 负载因子时触发扩容)。
- 扩容规则:新容量 = 旧容量 × 2,同时重新计算所有元素的哈希索引(“rehash”),会消耗一定性能。
- 高效性能:
- 理想情况下,插入、查询、删除的时间复杂度均为 O(1)(直接通过哈希值定位桶)。
- 哈希冲突较少时,性能接近理想值;冲突严重(链表过长)时,性能会下降(红黑树优化可缓解此问题)。
二、HashMap 常用方法
HashMap 提供了丰富的方法操作键值对,以下是开发中最常用的方法,均附完整示例代码,可直接复制运行。
1. 基础操作:添加、获取、删除
(1)添加键值对(put ())
V put(K key, V value):添加键值对,若 Key 已存在则覆盖 Value,返回旧 Value(若 Key 不存在则返回 null)。void putAll(Map<? extends K, ? extends V> m):将另一个同类型 Map 的所有键值对添加到当前 HashMap 中(重复 Key 会被覆盖)。
import java.util.HashMap;
public class HashMapPutDemo {
public static void main(String[] args) {
// 1. 单个键值对添加
HashMap<String, Integer> scoreMap = new HashMap<>();
Integer oldMathScore = scoreMap.put("数学", 90); // Key 不存在,返回 null
System.out.println("旧数学成绩:" + oldMathScore); // 输出:null
scoreMap.put("语文", 85);
scoreMap.put("英语", 95);
System.out.println("添加后:" + scoreMap); // 输出:{数学=90, 语文=85, 英语=95}
// 重复 Key 覆盖:数学成绩从 90 改为 98
Integer updatedOldScore = scoreMap.put("数学", 98);
System.out.println("被覆盖的旧数学成绩:" + updatedOldScore); // 输出:90
System.out.println("覆盖后:" + scoreMap); // 输出:{数学=98, 语文=85, 英语=95}
// 2. 批量添加(putAll())
HashMap<String, Integer> extraScoreMap = new HashMap<>();
extraScoreMap.put("物理", 88);
extraScoreMap.put("化学", 92);
scoreMap.putAll(extraScoreMap);
System.out.println("批量添加后:" + scoreMap);
// 输出:{数学=98, 语文=85, 英语=95, 物理=88, 化学=92}
}
}
(2)获取值与判断存在(get ()、containsKey ()、containsValue ())
V get(Object key):根据 Key 获取 Value,若 Key 不存在则返回 null(注意:若 Value 本身是 null,需用containsKey()区分 “Key 不存在” 和 “Value 为 null”)。boolean containsKey(Object key):判断 HashMap 是否包含指定 Key,返回布尔值。boolean containsValue(Object value):判断 HashMap 是否包含指定 Value,返回布尔值。
public class HashMapGetContainsDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("语文", 85);
scoreMap.put("生物", null); // Value 为 null
// 1. 根据 Key 获取 Value
Integer mathScore = scoreMap.get("数学");
Integer historyScore = scoreMap.get("历史"); // Key 不存在
Integer bioScore = scoreMap.get("生物"); // Value 本身是 null
System.out.println("数学成绩:" + mathScore); // 输出:98
System.out.println("历史成绩(Key 不存在):" + historyScore); // 输出:null
System.out.println("生物成绩(Value 为 null):" + bioScore); // 输出:null
// 2. 判断 Key 是否存在(区分“Key 不存在”和“Value 为 null”)
boolean hasBioKey = scoreMap.containsKey("生物");
boolean hasHistoryKey = scoreMap.containsKey("历史");
System.out.println("是否包含 Key '生物':" + hasBioKey); // 输出:true
System.out.println("是否包含 Key '历史':" + hasHistoryKey); // 输出:false
// 3. 判断 Value 是否存在
boolean has98 = scoreMap.containsValue(98);
boolean has100 = scoreMap.containsValue(100);
System.out.println("是否包含 Value 98:" + has98); // 输出:true
System.out.println("是否包含 Value 100:" + has100); // 输出:false
}
}
(3)删除键值对(remove ())
V remove(Object key):根据 Key 删除键值对,返回被删除的 Value(若 Key 不存在则返回 null)。
public class HashMapRemoveDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("语文", 85);
scoreMap.put("英语", 95);
// 删除 Key 为“语文”的键值对
Integer removedChineseScore = scoreMap.remove("语文");
System.out.println("被删除的语文成绩:" + removedChineseScore); // 输出:85
System.out.println("删除后:" + scoreMap); // 输出:{数学=98, 英语=95}
// 删除不存在的 Key
Integer removedHistoryScore = scoreMap.remove("历史");
System.out.println("删除不存在的 Key 返回值:" + removedHistoryScore); // 输出:null
}
}
2. 进阶操作:修改、清空、判断空否
(1)修改 Value(replace ())
V replace(K key, V value):仅当 Key 存在时,用新 Value 替换旧 Value,返回旧 Value(若 Key 不存在则返回 null,区别于put():put()会新增不存在的 Key)。
public class HashMapReplaceDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("英语", 95);
// 修改存在的 Key(英语成绩从 95 改为 97)
Integer oldEnglishScore = scoreMap.replace("英语", 97);
System.out.println("被修改的旧英语成绩:" + oldEnglishScore); // 输出:95
System.out.println("修改后:" + scoreMap); // 输出:{数学=98, 英语=97}
// 修改不存在的 Key(不会新增,返回 null)
Integer oldHistoryScore = scoreMap.replace("历史", 80);
System.out.println("修改不存在的 Key 返回值:" + oldHistoryScore); // 输出:null
System.out.println("修改后集合:" + scoreMap); // 输出:{数学=98, 英语=97}(无变化)
}
}
(2)清空与判断空否(clear ()、isEmpty ()、size ())
void clear():清空 HashMap 中所有键值对(集合变为空,对象本身仍存在)。boolean isEmpty():判断 HashMap 是否为空(元素个数为 0),返回布尔值。int size():返回 HashMap 中键值对的实际个数(区别于 “容量”)。
public class HashMapClearEmptySizeDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("英语", 97);
// 1. 获取集合大小
System.out.println("初始元素个数:" + scoreMap.size()); // 输出:2
// 2. 判断是否为空
System.out.println("初始是否为空:" + scoreMap.isEmpty()); // 输出:false
// 3. 清空集合
scoreMap.clear();
System.out.println("清空后元素个数:" + scoreMap.size()); // 输出:0
System.out.println("清空后是否为空:" + scoreMap.isEmpty()); // 输出:true
}
}
3. HashMap 三种核心遍历方式
HashMap 存储的是 “键值对(Entry)”,遍历需围绕 “Key 集合”“Value 集合”“Entry 集合” 展开,三种常用方式如下:
(1)遍历 Key 集合,再获取 Value(keySet ())
通过 keySet() 获取所有 Key 的集合,遍历 Key 后用 get(key) 获取对应 Value,适合仅需 Key 或需通过 Key 处理 Value 的场景。
public class HashMapKeySetDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("语文", 85);
scoreMap.put("英语", 97);
// 遍历 Key 集合
for (String subject : scoreMap.keySet()) {
Integer score = scoreMap.get(subject);
System.out.println(subject + ":" + score);
}
// 输出(顺序不固定):
// 数学:98
// 语文:85
// 英语:97
}
}
(2)直接遍历 Entry 集合(entrySet ())
通过 entrySet() 获取所有键值对(Map.Entry<K, V>)的集合,直接获取 Key 和 Value,效率最高(无需二次 get(key) 查询),是开发首选。
public class HashMapEntrySetDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("语文", 85);
scoreMap.put("英语", 97);
// 遍历 Entry 集合(推荐)
for (HashMap.Entry<String, Integer> entry : scoreMap.entrySet()) {
String subject = entry.getKey(); // 获取 Key
Integer score = entry.getValue(); // 获取 Value
System.out.println(subject + ":" + score);
}
// 输出(顺序不固定):
// 数学:98
// 语文:85
// 英语:97
}
}
(3)遍历 Value 集合(values ())
通过 values() 获取所有 Value 的集合,仅遍历 Value,适合无需 Key、仅需处理 Value 的场景(无法通过 Value 反向获取 Key)。
public class HashMapValuesDemo {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("数学", 98);
scoreMap.put("语文", 85);
scoreMap.put("英语", 97);
// 遍历 Value 集合
System.out.println("所有成绩:");
for (Integer score : scoreMap.values()) {
System.out.println(score);
}
// 输出(顺序不固定):
// 98
// 85
// 97
}
}
三、HashMap 使用注意事项
-
Key 的选择原则:
- Key 必须重写
hashCode()和equals()方法(否则无法正确判断 Key 唯一性,导致哈希冲突无法解决)。 - 推荐使用不可变类作为 Key(如
String、Integer):若 Key 是可变对象,修改后哈希值变化,会导致无法通过原 Key 获取 Value。 - 示例:若用
User类作为 Key,需手动重写方法:
- Key 必须重写
class User {
private String id;
// 重写 hashCode() 和 equals()
@Override
public int hashCode() { return id.hashCode(); }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
}
-
null 键值的注意事项:
- Key 最多 1 个 null,重复添加会覆盖;Value 可多个 null。
- 用
get(key)获取 Value 时,若返回 null,需通过containsKey(key)确认是 “Key 不存在” 还是 “Value 为 null”。
-
线程安全问题:
- 单线程环境:直接使用 HashMap 即可。
- 多线程环境:
- 若需弱一致性:使用
Collections.synchronizedMap(new HashMap<>())(对整个 HashMap 加锁,性能较低)。 - 若需高性能:优先使用
ConcurrentHashMap(JDK 1.8+ 采用分段锁,性能优于同步 HashMap)。
- 若需弱一致性:使用
-
性能优化技巧:
- 初始容量指定:若已知元素数量,创建时指定初始容量(如
new HashMap<>(100)),避免频繁扩容(扩容需 rehash,消耗性能)。 - 负载因子调整:默认 0.75 是 “性能与空间” 的平衡,若内存充足可降低(如 0.5,减少哈希冲突),若内存紧张可提高(如 0.8,减少数组占用空间)。
- 避免哈希冲突:合理重写 Key 的
hashCode()方法,尽量让哈希值均匀分布,减少链表 / 红黑树的长度。
- 初始容量指定:若已知元素数量,创建时指定初始容量(如
与 TreeMap/Hashtable 的区别(避免混淆):
| 特性 | HashMap | TreeMap | Hashtable(不推荐) |
|---|---|---|---|
| 排序 | 无序(按哈希值) | 有序(Key 自然排序 / 自定义排序) | 无序 |
| 线程安全 | 非线程安全 | 非线程安全 | 线程安全(全方法同步) |
| null 允许 | Key 1 个 null,Value 多个 null | 不允许 null | 不允许 null |
| 底层结构 | 数组 + 链表 / 红黑树 | 红黑树 | 数组 + 链表 |
| 适用场景 | 通用高效查询 | 需要有序键值对 | 遗留多线程场景(已被 ConcurrentHashMap 替代) |
四、总结
HashMap 是 Java 键值对集合的核心实现,核心优势在于 “哈希表” 带来的 O (1) 高效性能,适合大多数无需有序、单线程的键值对存储场景。掌握其底层结构(数组 + 链表 / 红黑树)、常用方法(put/get/remove/ 遍历)及使用注意事项(Key 重写方法、线程安全、性能优化),就能在开发中灵活应对各类场景。
4915

被折叠的 条评论
为什么被折叠?



