Map 与 HashMap
本教程共 100 篇 · 第 78 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
78. Map 与 HashMap
本节目标:认清 Map 是「键→值」配对容器、键不能重复,熟练用 HashMap 的增删查改与三种遍历视图,学完能正确用 Map 存查数据。
Map 存的是「配对」
前面学的 List、Set 都装单个元素,Map 不一样,它装的是「键 → 值」这种配对,像通讯录里「名字 → 号码」。
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
scores.put("李四", 85);
System.out.println(scores.get("张三")); // 90
```java
Map 有个铁律:**键不能重复**。你往同一个键上再 put 一次,新值会覆盖旧值,而不是变成两条。
```java
scores.put("张三", 95); // 覆盖,张三现在 95
System.out.println(scores.get("张三")); // 95
Note取一个不存在的键,HashMap 返回
null。所以get返回 null 有两种可能:键不存在,或者键对应的值本来就是 null。需要区分时先containsKey判断。
HashMap 是最常用的实现
HashMap 是 Map 的默认实现,底层也是哈希表,所以查找极快、不保序。日常 90% 的键值场景用它就够了。
Map<String, String> env = new HashMap<>();
env.put("JAVA_HOME", "/opt/java");
env.put("PATH", "/usr/bin");
boolean has = env.containsKey("JAVA_HOME"); // 是否含某个键
String v = env.getOrDefault("TEMP", "/tmp"); // 没有就给默认值
env.remove("PATH"); // 按键删
int n = env.size(); // 键值对个数
```bash
`v = scores.getOrDefault("王五", 0)`。注意 `get` 和 `getOrDefault` 的区别:后者在键缺失时返回你给的默认值,前者返回 null,写统计类逻辑时 `getOrDefault` 顺手。
## 三种遍历视图
Map 不能直接用 for-each 遍历,因为它既不是单列也不是数组。它提供三个「视图」让你间接遍历:
```java
// 1. 遍历所有键
for (String key : scores.keySet()) {
System.out.println(key);
}
// 2. 遍历所有值
for (Integer val : scores.values()) {
System.out.println(val);
}
// 3. 遍历键值对(最常用)
for (var entry : scores.entrySet()) {
System.out.println(entry.getKey() + " => " + entry.getValue());
}
Tip要同时拿到键和值,永远优先用
entrySet(),比先keySet()再挨个get(key)快——后者等于每个键又查了一次表。
键的约束和 Set 一样
Map 判断两个键是否「相同」,用的也是 equals + hashCode 两条约定,和 HashSet 判重一模一样。所以把自定义对象当键时,同样要保证这两个方法基于业务字段正确重写,而且放进 Map 后别再改参与 hashCode 的字段,否则就找不回来了。
record City(String code) {} // 自动生成 equals/hashCode,适合当键
Map<City, Integer> pop = new HashMap<>();
pop.put(new City("BJ"), 2189);
```bash
HashMap 允许一个 `null` 键和多个 `null` 值,但 TreeMap(下一章)不允许 null 键。写代码时如果键可能为 null,心里要有数。
## 一个能跑的完整例子
下面这段统计词语出现次数,是 Map 最经典的应用:
```java
import java.util.*;
public class WordCount {
public static void main(String[] args) {
List<String> words = List.of("a", "b", "a", "c", "b", "a");
Map<String, Integer> freq = new HashMap<>();
for (String w : words) {
freq.put(w, freq.getOrDefault(w, 0) + 1); // 没有就从 0 起加
}
System.out.println(freq); // {a=3, b=2, c=1}
}
}
freq.getOrDefault(w, 0) + 1 这行是关键:第一次见到词给 0 再加 1,之后每次累加,一行就完成了计数。
Map 不是 Collection 家族的
回头看第 72 章那张家族图:Collection 是单列,Map 是单独一脉。所以 Map 没有实现 Collection 接口,它不能像 List/Set 那样直接 for-each(必须用 entrySet 等视图)。这是很多初学者困惑的点——为什么 Map 看起来像集合,却不能用集合的遍历方式。记住它「独立成派」即可。
一个键对应多个值怎么办
Map 规定一个键只能对应一个值,但现实里常要「一个学生对应多门成绩」。做法是把值设成集合:Map<String, List<Integer>>。
Map<String, List<Integer>> grades = new HashMap<>();
grades.computeIfAbsent("张三", k -> new ArrayList<>()).add(90);
grades.computeIfAbsent("张三", k -> new ArrayList<>()).add(85);
```bash
`computeIfAbsent` 的意思是:键不存在就先用后面的函数造一个值放进去,存在就直接返回。这行代码一行就完成了「没有就建列表、有就往里加」,是 Map 多值写法的利器。
## 遍历时删键也要用迭代器
第 81 章说过,遍历集合时改结构会触发 fail-fast。Map 同理:你不能边 for-each 边 `map.remove(key)`,得用 `entrySet().iterator().remove()`。
```java
Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue() < 60) {
it.remove(); // 安全删掉不及格的
}
}
Warning直接
for (var e : map.entrySet()) { map.remove(e.getKey()); }会抛ConcurrentModificationException。要删就走迭代器的 remove,这是 Map 遍历删除的唯一安全通道。
容量和负载因子(了解即可)
HashMap 构造时可以给初始容量和负载因子(默认 0.75)。容量不够时会扩容并重排,和 ArrayList 扩容类似。数据量大、能预估规模时,给个初始容量能减少扩容次数。日常用默认即可,不必纠结。
put 返回的旧值,别浪费
put 方法其实有返回值:如果键之前存在,它返回被覆盖掉的旧值;如果是新键,返回 null。这个返回值在很多场景有用,比如「记录某个键上一次是什么」。
Integer old = scores.put("张三", 95);
System.out.println(old); // 第一次放打印 null,否则打印被覆盖的旧分
```bash
> [!TIP]
> 想判断「是不是第一次放这个键」,直接看 put 的返回值是不是 null 就行,不用先 containsKey 再 put,少一次查表。
## 键用什么类型最稳
Map 的键最好用「不可变且正确实现 equals/hashCode」的类型,最稳的就是 `String`、`Integer` 这些 JDK 自带的。自己写的类当键时,强烈建议用 `record`(自动生成正确契约且不可变)。一旦键对象在放进 Map 后被改了参与 hashCode 的字段,它就「失踪」了——再也查不到,这是 Map 最阴间的 bug 之一,务必警惕。
## 小提醒:value 不要求唯一
Map 只保证 key 唯一,value 完全可以重复。比如统计每个学生考了哪几门,value 都是分数,重复很正常。别误以为 Map 连 value 也去重,那是 Set 的活。记住:唯一性只约束在 key 上,value 随你放。所以「去重」永远发生在 key 维度,value 维度想去重得另想办法,比如把 value 也丢进一个 Set 里单独处理。
另外,Map 本身没有实现 Collection,所以你不能把它直接当成集合去调 `removeIf` 之类的方法;要批量操作,得通过 `keySet`/`values`/`entrySet` 这三个视图,它们才是真正的 Collection,能调用集合的所有方法。
## 小结
Map 是键值配对容器,键不能重复、会覆盖;HashMap 是最常用的实现,基于哈希表、查找快但不保序。增删查改用 put/get/containsKey/remove,遍历优先用 entrySet。键同样依赖 equals/hashCode 约定。下一章看 LinkedHashMap、TreeMap,以及该绕开的 Hashtable。