第17章 集合框架:Map
上一节学习了 List 与 Set,本节学习双列集合 Map------以"键值对"形式存储数据,是 Java 中使用频率最高的集合之一(如缓存、统计、对象属性映射)。
一、什么是 Map
Map 存储键值对(Key-Value),每个键唯一,一个键对应一个值:
键(key) 值(value)
"name" -> "张三"
"age" -> 18
"city" -> "北京"
- 键不能重复,值可以重复
- 通过键来获取值(像字典查单词)
- 键和值可以是任何引用类型
为什么需要 Map?List/数组只能按下标找元素,而现实数据往往是"按名字找内容"(学号找学生、商品名找价格)。List 要遍历比对,Map 直接按键取数:
java
// List 找"李四"的分数:要遍历(O(n))
List<String> names = Arrays.asList("张三", "李四");
List<Integer> scores = Arrays.asList(85, 92);
for (int i = 0; i < names.size(); i++) {
if (names.get(i).equals("李四")) System.out.println(scores.get(i)); // 92
}
// Map 直接按键取(O(1))
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 85);
scoreMap.put("李四", 92);
System.out.println(scoreMap.get("李四")); // 92
二、Map 的常用实现
| 实现 | 特点 |
|---|---|
| HashMap | 最常用,无序,基于哈希表,键可存一个 null |
| LinkedHashMap | 保持插入顺序 |
| TreeMap | 按键自动排序(键须可比较) |
| Hashtable | 线程安全但效率低,已不推荐 |
开发中用 HashMap 就够了:保序用 LinkedHashMap,排序用 TreeMap,多线程并发用 ConcurrentHashMap(见扩展知识)。
三、HashMap 基本操作

java
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
// 添加 / 修改:键不存在是添加,键已存在是覆盖(更新值)
scores.put("张三", 85);
scores.put("李四", 92);
scores.put("王五", 78);
scores.put("张三", 90); // 张三的值被更新为 90
// 获取
System.out.println(scores.get("张三")); // 90
System.out.println(scores.get("赵六")); // null:键不存在
// 判断
System.out.println(scores.containsKey("李四")); // true
System.out.println(scores.containsValue(78)); // true
// 删除
scores.remove("王五");
// 大小
System.out.println(scores.size()); // 2
// 取默认值:键不存在时返回默认值,不返回 null
int score = scores.getOrDefault("赵六", 0); // 0
几个"返回值有讲究"的方法,面试常问:
java
// put 返回被覆盖的旧值;键不存在时返回 null
Map<String, Integer> m = new HashMap<>();
System.out.println(m.put("a", 1)); // null:第一次放,没有旧值
System.out.println(m.put("a", 2)); // 1:返回被覆盖的旧值
System.out.println(m.get("a")); // 2
// remove 返回被删掉的值;putAll 批量合并;clear 清空
Map<String, Integer> other = new HashMap<>();
other.put("b", 3);
m.putAll(other);
System.out.println(m.remove("a")); // 2
如果你用的是 JDK 11 或 17,创建少量固定键值对还有更快的写法------Map.of 一行搞定,生成的是不可变 Map(JDK 8 没有这个方法,只能手动 put):
java
// 需 JDK 11/17 才能运行(JDK 8 没有 Map.of)
Map<String, Integer> quick = Map.of("张三", 90, "李四", 92);
System.out.println(quick); // {张三=90, 李四=92}
四、遍历 Map 的四种方式

java
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
scores.put("李四", 92);
// 方式一:遍历键值对(最常用、效率最高)
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 方式二:遍历键,再取对应值
for (String key : scores.keySet()) {
System.out.println(key + " -> " + scores.get(key));
}
// 方式三:只遍历值
for (Integer value : scores.values()) {
System.out.println(value);
}
// 方式四:Lambda(JDK 8+)
scores.forEach((key, value) -> System.out.println(key + " -> " + value));
推荐方式一和方式四:一次遍历拿全键和值,避免二次查询;方式二每次 get(key) 都重新算哈希,数据量大时更慢。
五、HashMap 的底层原理(面试高频)
HashMap 的核心是"数组 + 链表 + 红黑树":
- 每个元素根据键的
hashCode()计算哈希值,确定在数组中的桶位置 - 多个键的哈希冲突(落在同一个桶)时,用链表 串联(JDK 8 后,链表超过 8 个转为红黑树提升查询效率)
- 数组默认初始容量 16,负载因子 0.75------元素达到 16×0.75=12 时扩容为原来的 2 倍
为什么查询快?理想情况下每个桶一个元素,时间复杂度 O(1),直接根据哈希定位,不用遍历(哈希定位细节见扩展知识 1)。
自定义类作为键时,必须重写 hashCode 和 equals(与 HashSet 同理):
java
Map<Student, String> map = new HashMap<>();
map.put(new Student("张三", 18), "A班");
map.get(new Student("张三", 18)); // 没重写 hashCode/equals 时返回 null!
六、Map 的经典应用

1. 统计单词出现次数
java
String text = "apple banana apple orange banana apple";
Map<String, Integer> count = new HashMap<>();
for (String word : text.split(" ")) {
count.put(word, count.getOrDefault(word, 0) + 1);
}
System.out.println(count); // {banana=2, orange=1, apple=3}
2. 分组统计
java
// 统计各分数段人数
Map<String, Integer> group = new HashMap<>();
int[] scores = {85, 92, 45, 78, 60, 99};
for (int s : scores) {
String level = s >= 90 ? "优秀" : s >= 60 ? "及格" : "不及格";
group.put(level, group.getOrDefault(level, 0) + 1);
}
System.out.println(group); // {及格=3, 优秀=2, 不及格=1}
3. 两数之和(LeetCode 经典)
java
// 在数组中找两个数,使其和等于 target,返回下标
int[] nums = {2, 7, 11, 15};
int target = 9;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (map.containsKey(need)) {
System.out.println(map.get(need) + ", " + i); // 0, 1
break;
}
map.put(nums[i], i);
}
用 Map 把时间复杂度从 O(n²) 降到 O(n),是"空间换时间"的经典思路;类似的还有 computeIfAbsent(JDK 8+)做缓存:键不存在才计算、存在直接用缓存值。
七、LinkedHashMap 与 TreeMap

java
// LinkedHashMap:保持插入顺序
Map<String, Integer> lhm = new LinkedHashMap<>();
lhm.put("c", 3); lhm.put("a", 1); lhm.put("b", 2);
System.out.println(lhm.keySet()); // [c, a, b]:插入顺序
// TreeMap:按键排序
Map<String, Integer> tm = new TreeMap<>();
tm.put("banana", 1); tm.put("apple", 2); tm.put("cherry", 3);
System.out.println(tm.keySet()); // [apple, banana, cherry]:字典序
TreeMap 和 TreeSet 一样支持传入自定义比较器(如按键长度排序)。
八、扩展知识
1. HashMap 的 put / get 完整流程
put(key, value) 的流程(JDK 8/11/17 一致):
- 计算
hash = key.hashCode() ^ (hashCode >>> 16)(高低位混合,减少冲突) - 计算桶下标
index = (数组长度 - 1) & hash - 桶为空 → 直接放入
- 桶不为空 → 用 equals 逐个比较 key,相同就覆盖 value 并返回旧值
- 没找到 → 追加到链表尾部(链表长度 ≥ 8 且数组长度 ≥ 64 时转成红黑树)
- 元素总数超过
容量 × 0.75→ 扩容为 2 倍,所有元素重新散列
get(key) 的流程: 算 hash → 算桶下标 → 桶为空返回 null → 第一个节点 key 相同直接返回 → 否则用 equals 逐个找。定位全靠 (长度 - 1) & hash 一个位运算,这就是 HashMap 快的原因。
2. ConcurrentHashMap 初识
HashMap 线程不安全:多个线程同时 put 时,可能互相覆盖、丢数据。多线程要用什么?
| 方案 | 说明 |
|---|---|
| Hashtable | 所有方法加锁(锁整张表),安全但慢,已不推荐 |
| Collections.synchronizedMap | 同样锁整张表 |
| ConcurrentHashMap | JDK 8 起采用"数组 + 链表 + 红黑树" + CAS + synchronized 锁单个桶,并发度高,推荐 |
java
import java.util.concurrent.ConcurrentHashMap;
// 多线程环境下用 ConcurrentHashMap
Map<String, Integer> safe = new ConcurrentHashMap<>();
safe.put("a", 1);
System.out.println(safe.get("a")); // 1
注意:ConcurrentHashMap 的迭代器是弱一致的,遍历时被其他线程修改不会抛异常(和 List 的 fail-fast 相反)。单线程用 HashMap,多线程共享用 ConcurrentHashMap。
3. Map.of 详解(需 JDK 11/17)
java
// 需 JDK 11/17 才能运行(JDK 8 没有 Map.of)
Map<String, Integer> m = Map.of("a", 1, "b", 2);
System.out.println(m); // {a=1, b=2}
// ❌ m.put("c", 3); // UnsupportedOperationException:不可变
// ❌ Map.of("a", null); // NullPointerException:键值都不能为 null
// 最多直接写 10 对键值;再多用 Map.ofEntries(...)
Map.of 适合"常量配置"这种固定不变的数据:代码简洁、天然防篡改。
九、易错点

1. 可变对象当 key,修改后查找失败
java
// ❌ 把可变对象当 key:修改内容后 hashCode 变了,原来的键"找不到了"
class Person {
String name;
Person(String name) { this.name = name; }
@Override public int hashCode() { return name.hashCode(); } // 重写:按 name 算哈希
@Override public boolean equals(Object o) { return o instanceof Person
&& name.equals(((Person) o).name); }
}
Map<Person, String> map = new HashMap<>();
Person p = new Person("张三");
map.put(p, "A班");
p.name = "李四"; // 修改了 key 的内容!
System.out.println(map.get(p)); // null:hashCode 变了,定位不到原桶
// ✅ 用不可变类型(String、Integer 等)当 key
Map<String, String> safe = new HashMap<>();
safe.put("张三", "A班");
System.out.println(safe.get("张三")); // A班
2. 遍历时删除 → 抛异常
java
Map<String, Integer> scores = new HashMap<>();
scores.put("a", 1); scores.put("b", 2); scores.put("c", 3);
// ❌ 遍历 keySet 时直接 remove:抛 ConcurrentModificationException
for (String key : scores.keySet()) {
if (key.equals("a")) scores.remove(key);
}
// ✅ 迭代器的 remove()
Iterator<String> it = scores.keySet().iterator();
while (it.hasNext()) {
if (it.next().equals("a")) it.remove();
}
System.out.println(scores); // {b=2, c=3}
3. HashMap 线程不安全
java
// ❌ 多线程同时往 HashMap 里 put:可能丢失数据、覆盖写
// 10 个线程各 put 1000 个不同 key,最终 size 常常 < 10000
// ✅ 换成 ConcurrentHashMap:结果稳定
Map<String, Integer> safe = new ConcurrentHashMap<>();
单线程内 HashMap 完全够用;一旦多个线程共享同一个 Map 并写入,立刻换 ConcurrentHashMap。
4. put 相同 key 会覆盖旧值
java
Map<String, Integer> map = new HashMap<>();
map.put("张三", 85);
// ❌ 想"只在不存在时添加",却把 85 覆盖成了 90
map.put("张三", 90);
System.out.println(map.get("张三")); // 90
// ✅ 用 putIfAbsent(JDK 8+):键已存在就不动它
Map<String, Integer> map2 = new HashMap<>();
map2.put("张三", 85);
map2.putIfAbsent("张三", 90);
System.out.println(map2.get("张三")); // 85:没有被覆盖
5. Map.of 不能存 null、不能修改(需 JDK 11/17)
java
// 需 JDK 11/17 才能运行
// ❌ Map.of("a", null); // NullPointerException
// ❌ Map.of("a", 1).put("b", 2); // UnsupportedOperationException
// ✅ 需要 null 值 → 用 HashMap
Map<String, String> hm = new HashMap<>();
hm.put("a", null);
System.out.println(hm.get("a")); // null
十、小结
- 增删改查:put / get / remove / containsKey;put 返回旧值,remove 返回被删的值
- 遍历推荐 entrySet 或 forEach,避免 keySet + get 二次查询
- HashMap 底层是数组+链表+红黑树,查询 O(1);put/get 靠 hash 定位、equals 找键
- 自定义类当 key 必须重写 hashCode + equals,且 key 不要被修改
- 多线程用 ConcurrentHashMap;JDK 11/17 的 Map.of 不可变、不能 null
下一节学习泛型。
下一篇:第18章 泛型入门