☕ 第七章 集合
📝 一句话概括

Map = 按名字存取的储物柜——键(柜号)唯一、值(东西)可重复,存和取都用名字。它不归 Collection 管(双列结构自成体系);HashMap 的键去重逻辑和 HashSet 一模一样,7.5 的规矩原样搬过来。

7.6 Map接口

1. 双列结构:Map 自成一个体系

Collection 家族存的是"一个个元素"(单列);Map 存的是一对对数据(双列):

Collection(单列):[波仔] [墨池] [AI]          —— 只管"装了什么"
Map(双列):       Java → 波仔                 —— 还管"叫什么名字"
                   CAD → 波仔
                   AI  → 墨池

一对数据的专业叫法:

注意: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 同名同义
💡 put 覆盖是个武器

「统计每个单词出现次数」的经典套路就靠它: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,值统一放一个占位对象)。键的去重照旧两道关卡:

  1. 调 key 的 hashCode() 定位桶
  2. 同桶再 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. 常见误区

⚠️ 误区 1:`for (K k : map)` 直接冒号遍历

Map 不是 Collection,没有迭代器可发,编译报错。必须先 keySet / values / entrySet 转成单列再遍历。

⚠️ 误区 2:以为 put 重复键会报错或多存一条

不报错、不新增——覆盖旧值并返回旧值。这正是计数器套路的地基。

⚠️ 误区 3:get 到 null 就当"出 bug 了"

get 没找到返回 null 是正常行为不报错。要区分"没有这个键"还是"值就是 null",用 containsKey 先问一句。

⚠️ 误区 4:自定义类当键不重写 equals/hashCode

和 HashSet 同一个坑:同内容对象进不同桶,get 查无此人。套餐规矩照旧:两个一起重写。

⚠️ 误区 5:以为 Map 是 Collection 的子接口

家族地图里它俩并排——Collection 单列管元素,Map 双列管映射,各自一棵树。

8. 自测一下

❓ Q1:Map 的键和值各有什么规矩?put 已存在的键会发生什么?

键唯一(重复则覆盖)、值可重复。put 已存在的键 → 新值覆盖旧值,返回被覆盖的旧值;新键返回 null。

❓ Q2:Map 为什么不能用增强 for 直接冒号遍历?三种姿势各自什么时候用?

Map 不是 Collection,发不出迭代器。键值都要 → entrySet(一次到位效率最好);以键为线索 → keySet 再 get;只要值 → values。

❓ Q3:HashMap 的键靠什么保证唯一?和 HashSet 什么关系?

同一套哈希表两道关卡:key 的 hashCode 定位桶 + equals 验身(相同覆盖)。HashSet 内部就是一个 HashMap——所以 7.5 的搭伴规矩原样适用。

❓ Q4:getOrDefault(word, 0) 干什么的?为什么计数器离不开它?

键存在返回对应值,不存在返回默认值 0。计数器要"第一次出现从 1 开始、之后加一",没有它会拿到 null 拆箱炸 NPE(6.1 的坑)。

❓ Q5(毕业题):统计一段话里每个字符出现的次数,写出核心循环并说明用到哪些旧知识。

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. 下一步学什么


相关笔记