Map 接口与键值对模型 | JavaSE

Map 接口与键值对模型

一、学习目标

完成本章后,你应该能够:

  • 能够解释 Map<K,V>Collection<E> 在数据模型上的根本区别。
  • 能够解释键(Key)、值(Value)、映射(Mapping)和键值对(Entry)。
  • 能够说明 Map 中 key 不允许重复,而 value 可以重复。
  • 能够解释重复 put() 同一个 key 时为什么会覆盖原 value。
  • 能够熟练使用 putgetremovecontainsKeycontainsValuesize 等核心 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 知识问答

  1. 什么是 Map?
  2. 为什么 Map 又叫键值对集合?
  3. Map 与 Collection 的核心区别是什么?
  4. KV 分别代表什么?
  5. key 能否重复?
  6. value 能否重复?
  7. 重复 put 同一个 key 会发生什么?
  8. put() 的返回值是什么?
  9. get() 的作用是什么?
  10. containsKey()containsValue() 有什么区别?
  11. remove(key) 删除什么?
  12. size() 统计的是 key 数、value 数还是 mapping 数?
  13. 为什么 keySet 返回 Set?
  14. 为什么 values 返回 Collection?
  15. 什么是 Map.Entry?
  16. entrySet 为什么适合遍历 key 和 value?
  17. Map 是否统一允许 null?
  18. 为什么说 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));

回答:

  1. 最终有几个 mapping?
  2. "Java" 对应什么 value?
  3. 为什么 size 没有因为第二次 put Java 而增加?
  4. 两个不同 key 是否可以对应相同 value?
  5. 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("张三不存在");
}

回答:

  1. 这段逻辑为什么可能错误?
  2. get() == null 有哪两种可能?
  3. 如何准确判断 key 是否存在?
  4. 为什么这个问题与具体 Map 是否允许 null value 有关?

8.5 综合训练

设计一个简单商品库存系统。

数据:

商品编号 → 库存数量

使用:

Map<String, Integer>

要求:

  1. 添加商品库存。
  2. 查询商品库存。
  3. 已存在商品再次入库时更新数量。
  4. 删除商品。
  5. 判断商品是否存在。
  6. 输出全部商品及库存。
  7. 分别使用 entrySetforEach 遍历。
  8. 说明为什么 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 业务模型。