Map 接口与键值对模型 | JavaSE
Map 接口与键值对模型
一、学习目标
完成本章后,你应该能够:
- 能够解释
Map<K,V>与Collection<E>在数据模型上的根本区别。 - 能够解释键(Key)、值(Value)、映射(Mapping)和键值对(Entry)。
- 能够说明 Map 中 key 不允许重复,而 value 可以重复。
- 能够解释重复
put()同一个 key 时为什么会覆盖原 value。 - 能够熟练使用
put、get、remove、containsKey、containsValue、size等核心 API。 - 能够解释
keySet()、values()、entrySet()为什么分别返回 Set、Collection、Set。 - 能够掌握 Map 的三种基本遍历方式。
- 能够使用
Map.Entry<K,V>表达一个键值映射。 - 能够理解 Map 中
null支持取决于具体实现,而不是 Map 接口统一保证。 - 能够根据现实业务识别适合
key → value建模的问题。
二、核心知识
2.1 Map 是什么
Map 可以翻译为:
映射。
它保存的不是:
一个元素
一个元素
一个元素
而是:
key → value
key → value
key → value
例如:
张三 → 95
李四 → 88
王五 → 91
在 Java 中:
Map<String, Integer>
可以表示:
学生姓名 → 成绩
2.2 Map 也叫键值对集合
Map 中每一组数据包含:
键(Key)
+
值(Value)
例如:
map.put("Java", 95);
可以理解成:
key = "Java"
value = 95
整体 Map:
{
Java=95,
MySQL=90,
Spring=92
}
2.3 Map 与 Collection 的根本区别
前面学习的:
List
Set
都属于:
Collection<E>
模型:
一个元素 E
例如:
List<String>
保存:
Java
MySQL
Redis
而 Map:
Map<K, V>
保存的是:
K → V
例如:
Map<String, Integer>
保存:
Java → 95
MySQL → 90
Redis → 88
所以:
Map 并不是 Collection 接口的子接口。
但 Map 仍然属于 Java Collections Framework 的重要组成部分。
2.4 Map 的泛型参数
Map<K, V>
其中:
K = Key 类型
V = Value 类型
例如:
Map<String, Integer>
表示:
key → String
value → Integer
例如:
Map<Long, User>
可以表达:
用户 ID → User 对象
2.5 key 不允许重复
这是 Map 最重要的规则。
例如:
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 80);
scores.put("张三", 95);
不是:
张三 → 80
张三 → 95
同时存在。
最终是:
张三 → 95
因为:
一个 Map 中,同一个 key 最多只能映射到一个 value。
2.6 value 可以重复
例如:
scores.put("张三", 95);
scores.put("李四", 95);
scores.put("王五", 88);
这是完全合法的。
因为:
不同学生
完全可以拥有:
相同成绩
所以 Map 的规则是:
key → 唯一
value → 可以重复
2.7 为什么特点主要由 key 决定
后面会学习:
HashMap
LinkedHashMap
TreeMap
它们最核心的差异都体现在:
key 如何组织。
例如:
HashMap
→ key 使用哈希组织
LinkedHashMap
→ key 的映射额外维护确定顺序
TreeMap
→ key 根据比较规则排序
因此原始教学资料中常说:
Map 的特点主要由键决定。
这是非常重要的理解。
2.8 常见 Map 实现
当前只建立整体认识:
Map
├── HashMap
├── LinkedHashMap
└── TreeMap
它们分别会在后续章节详细学习。
本章重点是:
所有 Map 共有的键值对模型和接口 API。
不要现在就深入 HashMap 扩容、红黑树等实现原理。
三、使用方法
3.1 创建 Map
Map 是接口,不能:
new Map<>();
通常:
Map<String, Integer> map = new HashMap<>();
这里:
Map
→ 接口类型
HashMap
→ 具体实现
体现面向接口编程。
3.2 put(K key, V value)
添加键值对:
map.put("Java", 95);
map.put("MySQL", 90);
map.put("Spring", 92);
3.3 put 的返回值
put() 的返回类型是:
V
例如:
Integer oldValue =
map.put("Java", 100);
如果 "Java" 之前映射:
95
那么:
oldValue = 95
而 Map 中变成:
Java → 100
因此:
put()不只是“添加”,还承担“新增或替换映射”的语义。
3.4 重复 key:覆盖旧 value
例如:
Map<String, Integer> map = new HashMap<>();
map.put("Java", 90);
map.put("Java", 100);
最终:
Java → 100
Map 的 size 仍然是:
1
因为 key 没有增加。
只是:
旧映射的 value
↓
被新 value 替换
3.5 get(key)
根据 key 查找 value:
Integer score = map.get("Java");
这体现了 Map 最核心的操作:
key
↓
快速找到
↓
value
例如:
用户 ID
↓
用户对象
或者:
商品编号
↓
商品信息
3.6 get 不存在的 key
如果没有这个 key:
map.get("Python");
按照 Map 接口的一般语义,会返回:
null
但是这里存在一个重要问题。
如果某个具体 Map 实现允许:
key → null
那么:
get(key) == null
可能有两种含义:
这个 key 不存在
或者:
这个 key 存在,但 value 就是 null
因此当需要严格区分时,应结合:
containsKey(key)
判断。
3.7 containsKey()
判断 Map 是否包含某个 key:
boolean exists =
map.containsKey("Java");
非常适合:
用户名是否存在
商品编号是否存在
学生学号是否存在
缓存中是否已经存在该 key
3.8 containsValue()
判断是否存在某个 value:
boolean exists =
map.containsValue(95);
需要注意:
containsKey
和:
containsValue
语义完全不同。
3.9 remove(key)
根据 key 删除整个映射:
Integer oldValue =
map.remove("Java");
删除的不是:
只删除 key
而是:
Java → 95
整个 mapping。
返回值是删除前关联的 value。
3.10 size()
int size = map.size();
表示:
Map 中有多少组键值映射。
不是:
key 数 + value 数
例如:
Java → 95
MySQL → 90
Spring → 92
size 是:
3
3.11 clear()
清空所有映射:
map.clear();
3.12 isEmpty()
判断是否没有任何键值映射:
boolean empty = map.isEmpty();
3.13 常用 API 总结
| API | 作用 |
| ---------------------- | --------------------------------- |
| put(K,V) | 新增或替换映射 |
| get(key) | 根据 key 获取 value |
| remove(key) | 删除 key 对应的映射 |
| containsKey(key) | 判断 key 是否存在 |
| containsValue(value) | 判断 value 是否存在 |
| size() | 获取映射数量 |
| isEmpty() | 判断是否为空 |
| clear() | 清空 Map |
| keySet() | 获取所有 key 的 Set 视图 |
| values() | 获取所有 value 的 Collection 视图 |
| entrySet() | 获取所有键值对 Entry 的 Set 视图 |
四、原理与进阶
4.1 什么叫 Mapping
Map 中的一组:
key → value
正式称为:
映射(Mapping)
例如:
10001 → 张三
表示:
key 10001
被映射到:
value 张三
4.2 Map.Entry 是什么
Java 使用:
Map.Entry<K, V>
表示 Map 中的一组键值映射。
例如:
Java → 95
可以抽象为一个 Entry:
Entry
├── key = Java
└── value = 95
主要 API:
entry.getKey();
entry.getValue();
因此:
Map.Entry可以理解为“把一组 key-value 当成一个对象来处理”。
4.3 为什么 keySet() 返回 Set
方法:
Set<K> keySet();
为什么不是:
List<K>
或者:
Collection<K>
核心原因是:
Map 中 key 不允许重复。
这正符合:
Set
的语义。
所以:
Map 的 key
↓
天然形成 Set
4.4 为什么 values() 返回 Collection
Collection<V> values();
为什么不是:
Set<V>
因为:
value 可以重复。
例如:
张三 → 95
李四 → 95
values:
95
95
不能自动去重。
因此最准确的抽象是:
Collection<V>
4.5 为什么 entrySet() 返回 Set
Set<Map.Entry<K, V>> entrySet();
Map 中每一个 key 都唯一对应一组 mapping。
例如:
Java → 95
不会同时存在两个完全独立的:
Java → ...
映射。
因此所有 Entry 可以组成:
Set<Map.Entry<K,V>>
4.6 keySet、values、entrySet 是“视图”
这是一个很重要的进阶概念。
例如:
Set<String> keys = map.keySet();
这里得到的不是:
与原 Map 完全无关的一份独立复制。
而是:
由 Map 支撑(backed by the map)的视图。
也就是说:
Map 改变
↓
视图会反映变化
某些通过视图进行的删除操作,也会影响原 Map。
因此:
View
和:
Copy
是两个完全不同的概念。
4.7 Map 是否允许 null
不能笼统说:
Map 可以存 null
也不能笼统说:
Map 不能存 null
准确口径应该是:
Map 接口允许具体实现自行决定是否支持 null key / null value。
例如:
HashMap
与某些其他 Map 实现对 null 的规则并不相同。
因此本章不能拿:
HashMap
的行为直接升级成:
所有 Map 的公共规则。
具体实现的 null 约束,应查对应实现类 API。
五、实践应用
5.1 学号 → 学生
Map<String, Student> students;
例如:
20260001 → Student对象
20260002 → Student对象
这种模型非常自然。
因为:
一个学号
↓
唯一定位
↓
一个学生
5.2 用户 ID → User
Map<Long, User>
这是企业项目中极其常见的模型。
5.3 商品 ID → 库存数量
Map<Long, Integer>
例如:
10001 → 20
10002 → 15
10003 → 0
5.4 单词 → 出现次数
Map<String, Integer>
例如:
Java → 5
Spring → 3
MySQL → 8
这就是 Map 最经典的统计模型:
对象
→
次数
5.5 国家 → 首都
Map<String, String>
例如:
China → Beijing
Japan → Tokyo
France → Paris
这就是现实世界的映射关系。
六、Map 的三种基本遍历
6.1 方式一:keySet + get
首先拿全部 key:
Set<String> keys = map.keySet();
再遍历:
for (String key : keys) {
Integer value = map.get(key);
System.out.println(
key + "=" + value
);
}
完整思路:
Map
↓
keySet()
↓
拿到所有 key
↓
逐个遍历 key
↓
map.get(key)
↓
找到 value
这种方式非常直观。
6.2 方式二:entrySet
Set<Map.Entry<String, Integer>> entries =
map.entrySet();
然后:
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(
key + "=" + value
);
}
这里直接得到:
key + value
因此如果遍历时两个都需要,entrySet 往往比:
key
↓
再 get(value)
更加直接。
6.3 方式三:forEach
Map 提供:
forEach(BiConsumer<? super K, ? super V> action)
可以写:
map.forEach((key, value) -> {
System.out.println(
key + "=" + value
);
});
Lambda 中:
第一个参数
→ key
第二个参数
→ value
Lambda 与 BiConsumer 的完整函数式编程知识将在后面的 Lambda 专章继续学习。
七、常见问题
7.1 Map 是 Collection 的子接口吗?
不是。
结构不是:
Collection
└── Map
而应该理解为:
Java Collections Framework
├── Collection
│ ├── List
│ └── Set
│
└── Map
它们属于同一个集合框架,但数据模型不同。
7.2 Map 为什么不能有重复 key?
因为 Map 表达:
key → value
如果同时存在:
Java → 95
Java → 80
那么执行:
map.get("Java")
就无法唯一决定返回哪个 value。
所以一个 key 只能对应一个当前 value。
7.3 一个 value 能对应多个 key 吗?
可以。
例如:
张三 → 95
李四 → 95
王五 → 95
完全合法。
7.4 put 是添加还是修改?
两者都是。
如果 key 不存在:
新增映射
如果 key 已经存在:
替换旧 value
所以更准确叫:
关联指定 value 到指定 key。
7.5 get 返回 null 一定表示 key 不存在吗?
不一定。
如果具体 Map 实现允许 null value:
key 存在但 value=null
也可能得到 null。
需要严格判断时使用:
containsKey(key)
7.6 为什么不能说“Map 都允许 null”?
因为 Map 是接口。
不同实现:
HashMap
TreeMap
ConcurrentHashMap
...
可能具有不同 null 约束。
应该查具体实现。
7.7 keySet 为什么不是 List?
因为 key:
不重复
因此 Set 才准确表达其语义。
7.8 values 为什么不是 Set?
因为 value:
允许重复
所以需要 Collection,而不能自动使用 Set。
7.9 Map.Entry 是一个新的 Map 吗?
不是。
一个 Entry 只表示:
一组 key → value 映射
而:
entrySet()
则获得 Map 中全部映射的集合视图。
八、练习与验收
8.1 知识问答
- 什么是 Map?
- 为什么 Map 又叫键值对集合?
- Map 与 Collection 的核心区别是什么?
K和V分别代表什么?- key 能否重复?
- value 能否重复?
- 重复 put 同一个 key 会发生什么?
put()的返回值是什么?get()的作用是什么?containsKey()与containsValue()有什么区别?remove(key)删除什么?size()统计的是 key 数、value 数还是 mapping 数?- 为什么 keySet 返回 Set?
- 为什么 values 返回 Collection?
- 什么是 Map.Entry?
- entrySet 为什么适合遍历 key 和 value?
- Map 是否统一允许 null?
- 为什么说 Map 的特点主要由 key 的组织方式决定?
8.2 代码阅读
不运行:
Map<String, Integer> map = new HashMap<>();
map.put("Java", 80);
map.put("MySQL", 90);
map.put("Java", 100);
map.put("Redis", 90);
System.out.println(map.size());
System.out.println(map.get("Java"));
System.out.println(map.containsKey("Spring"));
System.out.println(map.containsValue(90));
回答:
- 最终有几个 mapping?
"Java"对应什么 value?- 为什么 size 没有因为第二次 put Java 而增加?
- 两个不同 key 是否可以对应相同 value?
containsValue(90)如何分析?
8.3 手写代码
任务一:课程成绩
创建:
Map<String, Integer>
保存:
Java → 95
数据库 → 88
计算机网络 → 90
要求完成:
- 添加;
- 查询;
- 修改;
- 删除;
- 判断课程是否存在;
- 输出课程数量。
任务二:电话簿
Map<String, String>
表达:
姓名 → 电话
支持:
- 添加联系人;
- 修改电话;
- 查询电话;
- 删除联系人;
- 判断联系人是否存在。
任务三:三种遍历
对于同一个 Map,分别使用:
keySet + get
entrySet
forEach
完成完整遍历。
禁止只会其中一种。
8.4 Debug
分析:
Map<String, Integer> map = new HashMap<>();
map.put("张三", null);
if (map.get("张三") == null) {
System.out.println("张三不存在");
}
回答:
- 这段逻辑为什么可能错误?
get() == null有哪两种可能?- 如何准确判断 key 是否存在?
- 为什么这个问题与具体 Map 是否允许 null value 有关?
8.5 综合训练
设计一个简单商品库存系统。
数据:
商品编号 → 库存数量
使用:
Map<String, Integer>
要求:
- 添加商品库存。
- 查询商品库存。
- 已存在商品再次入库时更新数量。
- 删除商品。
- 判断商品是否存在。
- 输出全部商品及库存。
- 分别使用
entrySet与forEach遍历。 - 说明为什么 Map 比
List<ProductStock>更直接表达“通过商品编号快速找到库存”的模型。
8.6 本章验收
- [ ] 能画出 Collection 与 Map 的关系。
- [ ] 能解释
Map<K,V>的两个泛型。 - [ ] 能准确说出 key 唯一、value 可重复。
- [ ] 能解释重复 put 的覆盖行为。
- [ ] 能使用 put/get/remove/containsKey。
- [ ] 能解释
put()的返回值。 - [ ] 能解释
get()==null的二义性。 - [ ] 能解释 keySet、values、entrySet 的返回类型。
- [ ] 能解释 Map.Entry。
- [ ] 能手写三种 Map 遍历。
- [ ] 不会把 HashMap 的 null 规则错误推广成所有 Map 的规则。
- [ ] 能识别现实中的 key → value 业务模型。