Map = 按名字存取的储物柜——键(柜号)唯一、值(东西)可重复,存和取都用名字。它不归 Collection 管(双列结构自成体系);HashMap 的键去重逻辑和 HashSet 一模一样,7.5 的规矩原样搬过来。
7.6 Map接口
1. 双列结构:Map 自成一个体系
Collection 家族存的是"一个个元素"(单列);Map 存的是一对对数据(双列):
Collection(单列):[波仔] [墨池] [AI] —— 只管"装了什么"
Map(双列): Java → 波仔 —— 还管"叫什么名字"
CAD → 波仔
AI → 墨池
一对数据的专业叫法:
- 键 key:柜号——不允许重复(两个柜子一个号,按名取货就乱了)
- 值 value:柜子里的东西——允许重复(波仔管两个柜子毫无问题)
注意:Map 没有继承 Collection——家族地图里它是和 Collection 并排的另一棵树(7.1 的地图此刻补全)。
2. 三兄弟:还是 HashMap 主角
| 实现 | 一句话 | 键的顺序 | 何时用 |
|---|---|---|---|
| HashMap | 主力,按名查最快 | 不保证 | 按名存取第一反应 |
| LinkedHashMap | HashMap + 排队绳 | 插入顺序 | 去重还要记先后 |
| TreeMap | 自动按键排序 | 键升序 | 要按键有序(进阶再细学) |
3. 第一个 HashMap:put 的覆盖行为
Map<String, String> map = new HashMap<>();
map.put("Java", "波仔"); // 存
map.put("CAD", "波仔"); // 值可以重复
map.put("AI", "墨池");
map.put("Java", "BoZai"); // 键已存在 → 覆盖旧值!返回被覆盖的"波仔"
System.out.println(map.get("Java")); // BoZai
System.out.println(map.get("C++")); // null —— 没这个柜号
System.out.println(map.containsKey("AI")); // true
System.out.println(map.size()); // 3
map.remove("CAD"); // 按名撤柜
| 方法 | 作用 | 备注 |
|---|---|---|
put(k, v) |
存放;键已存在则覆盖并返回旧值 | 新键返回 null |
get(k) |
按名取值 | 没有返回 null(不报错) |
remove(k) |
按名删除整对 | 返回被删的值 |
containsKey(k) |
查键在不在 | 比 containsKey(value) 常用得多 |
size() / isEmpty() / clear() |
和 Collection 同名同义 |
「统计每个单词出现次数」的经典套路就靠它:map.put(word, map.getOrDefault(word, 0) + 1)——getOrDefault 没有给 0,有则取旧值加一再放回。两行顶一个循环。
4. 遍历三姿势:keySet / values / entrySet
Map 没有下标、不是 Collection,增强 for 直接冒号遍历 map 是编译报错——要遍历,先把"要的东西"取出来:
姿势一:keySet() —— 拿钥匙串(最常用)
for (String key : map.keySet()) { // keySet 返回所有键的 Set
System.out.println(key + " -> " + map.get(key)); // 拿键再取值,两步
}
姿势二:values() —— 只盘货
for (String v : map.values()) { // 只要值不关心键时用
System.out.println(v);
}
姿势三:entrySet() —— 键值打包,一次到位
for (Map.Entry<String, String> e : map.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue()); // 键值一次全拿
}
怎么选:键值都要 → entrySet(不用回头再 get,效率也最好);只要键或以键为线索 → keySet;只要值 → values。记忆法:keySet 拿钥匙串去开柜 · values 只盘货 · entrySet 整柜搬走一遍过。
5. HashMap 的键为什么唯一:7.5 的规矩原样生效
HashMap 底层和 HashSet 是同一套哈希表(事实上 HashSet 内部就是一个 HashMap,值统一放一个占位对象)。键的去重照旧两道关卡:
- 调 key 的 hashCode() 定位桶
- 同桶再 equals() 验明正身——相同就覆盖值,不同就同桶共存
所以 7.5 的结论原封不动搬过来:自定义类当 Map 的键,equals + hashCode 必须搭伴重写——否则同内容的不同对象进不同桶,get 找不到、键重复没人拦。
List 排队、Set 去重、Map 按名查。面试常问"你用过哪些集合",按这句答再各配一句底层(数组/哈希表),基础关就过了。
6. 动手试一试(30 秒实验)
新建 Demo06(import java.util.*;):
实验 1:put "Java"→1,再 put "Java"→2,打印 map——看键没多、值被覆盖;用变量接住第二次 put 的返回值——是被覆盖的旧值 1。
实验 2:map.get("不存在的键") 打印——null,不报错;再试 map.get(0)——编译报错,键类型是 String(泛型安检)。
实验 3:同一张 map 用 keySet、entrySet 各遍历一遍,对照输出——再看 for (String s : map) 直接冒号遍历——编译报错(Map 不是 Collection)。
实验 4:单词计数器:把 {"java","cad","java","ai","java"} 逐个 map.put(w, map.getOrDefault(w, 0) + 1),打印——{java=3, cad=1, ai=1},put 覆盖特性的实战。
实验 5:LinkedHashMap 存同样的键值——遍历顺序 = 插入顺序;换 TreeMap——键自动升序。
7. 常见误区
Map 不是 Collection,没有迭代器可发,编译报错。必须先 keySet / values / entrySet 转成单列再遍历。
不报错、不新增——覆盖旧值并返回旧值。这正是计数器套路的地基。
get 没找到返回 null 是正常行为不报错。要区分"没有这个键"还是"值就是 null",用 containsKey 先问一句。
和 HashSet 同一个坑:同内容对象进不同桶,get 查无此人。套餐规矩照旧:两个一起重写。
家族地图里它俩并排——Collection 单列管元素,Map 双列管映射,各自一棵树。
8. 自测一下
键唯一(重复则覆盖)、值可重复。put 已存在的键 → 新值覆盖旧值,返回被覆盖的旧值;新键返回 null。
Map 不是 Collection,发不出迭代器。键值都要 → entrySet(一次到位效率最好);以键为线索 → keySet 再 get;只要值 → values。
同一套哈希表两道关卡:key 的 hashCode 定位桶 + equals 验身(相同覆盖)。HashSet 内部就是一个 HashMap——所以 7.5 的搭伴规矩原样适用。
键存在返回对应值,不存在返回默认值 0。计数器要"第一次出现从 1 开始、之后加一",没有它会拿到 null 拆箱炸 NPE(6.1 的坑)。
for (char c : str.toCharArray()) map.put(c, map.getOrDefault(c, 0) + 1);——用到的旧知识:String 的 toCharArray/遍历(6.2)、自动装箱 char→Character(6.1)、getOrDefault 防空、put 覆盖累加(本节)。遍历 entrySet 输出即可。
9. 下一步学什么
- ☑ 7.7 常用工具类 ✅ —— 点此复习:Collections 管集合(sort/shuffle/reverse/addAll/max + 不可变包装)、Arrays 管数组(toString 收 2.7 旧账、binarySearch 先排序、asList 定长坑)。
相关笔记
- 第七章 集合/7.5 Set接口 ← 上一篇:HashSet 的两道关卡,本节原样用于键
- 第七章 集合/7.1 集合概述:家族地图里 Map 这棵树,本节正式走完
- 第六章 Java API/6.1 包装类:getOrDefault + 自动装箱,计数器的地基(null 拆箱 NPE 的坑)
- 第四章 面向对象(下)/4.5 Object类:自定义类当键,equals/hashCode 搭伴规矩再度生效