主题
界面
主题
《数组》 的结尾留了三个问题:长度固定、无法去重、无法表达"键 → 值"的映射。集合框架解决它们。
本节是三节集合课的总览:先建立"什么场景用什么集合"的判断力,再在后续两节深入具体实现(《ArrayList 与 LinkedList》、《HashMap》)。
完成本节后,你应该能够:
List、Set、Map 三者的语义差别,并做对选型;List.of / Set.of / Map.of 创建不可变集合,并知道它们的边界;entrySet() 正确遍历 Map,并说清为什么不用 keySet() 再 get;Arrays.asList、遍历中修改、== 比较元素这三类高频错误。equals 与 hashCode 的作用吗?(《equals、hashCode 与对象比较》)数组能解决"多个同类型数据",但实际需求往往更复杂:
| 需求 | 数组 | 集合 |
|---|---|---|
| 动态添加元素 | 需要自己扩容复制 | add 即可 |
| 自动去重 | 自己写双重循环 | Set 一行 |
| 按 key 查找 | 遍历比对 | Map.get |
| 判空、包含、批量操作 | 手写循环 | 现成 API |
集合的选择直接决定代码复杂度。选错集合的典型症状:明明该用 Map,却写了三层循环去查找。
Collection
├── List:有序、可重复、可按下标访问 (列表)
└── Set :不重复 (集合)
Map :键值对,key 唯一(不属于 Collection) (映射)| 接口 | 是否有序 | 是否允许重复 | 访问方式 | 典型用途 |
|---|---|---|---|---|
List | 是(插入序) | 是 | 下标 get(i) | 有序数据、结果集 |
Set | 取决于实现 | 否 | 遍历 | 去重、存在性判断 |
Map | 取决于实现 | key 唯一 | key 查找 | 缓存、计数、映射 |
一句话选型
ListSetMapList<String> list = new ArrayList<>();
list.add("a");
list.add(0, "b"); // 指定位置插入
list.get(0); // 按下标取
list.contains("a"); // 是否存在(依赖 equals)
list.remove("a"); // 按值删除(第一次出现)
list.remove(0); // 按下标删除
list.size(); // 元素个数
list.isEmpty(); // 是否为空(推荐用这个而不是 size() == 0)
list.clear();Set 与 Map 的对应操作:
Set<String> set = new HashSet<>();
set.add("a");
set.contains("a");
set.remove("a");
Map<String, Integer> map = new HashMap<>();
map.put("apple", 3);
map.get("apple");
map.getOrDefault("cherry", 0); // 不存在时给默认值
map.containsKey("apple");
map.remove("apple");Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("张三", 90);
scores.put("李四", 85);
// 推荐:一次拿到 key 与 value
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}不推荐:
for (String key : scores.keySet()) {
Integer value = scores.get(key); // 多一次查找
}keySet() 再 get() 会对每个 key 做一次额外查找。数据量大时差距明显——遍历 Map 就用 entrySet()。
Map 还有两个视图:keySet()(所有 key 的集合)与 values()(所有 value 的集合),它们都是视图,不是副本,改视图会影响原 Map。
List<String> list = List.of("x", "y"); // 不可变
Set<String> set = Set.of("a", "b"); // 不可变
Map<String, Integer> map = Map.of("a", 1, "b", 2); // 不可变
List<String> copy = List.copyOf(mutableList); // 创建当时的快照三条边界:
add 抛 UnsupportedOperationException;List.of("x", null) 直接抛 NullPointerException;Set.of / Map.of 不允许重复元素(重复直接抛异常)。适用场景:常量数据、方法返回值、传给别人的只读快照。这是 《封装》 里"防御性拷贝"的标准工具。
// 1. 增强 for:最常用
for (String item : list) { }
// 2. 迭代器:需要在遍历中删除元素时用(见 S1-019)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) { iterator.next(); }
// 3. forEach + lambda(Java 8+)
list.forEach(item -> System.out.println(item));List<String> list = new ArrayList<>(List.of("a", "b"));
String[] array = list.toArray(new String[0]); // List → 数组
List<String> fromArray = Arrays.asList(array); // 数组 → List(固定长度!)
List<String> immutable = List.of(array); // 数组 → 不可变 ListArrays.asList 的坑:返回的 List 长度固定,可以 set 但不能 add / remove——因为它只是数组的包装,不是真正的 ArrayList。
List<String> list = new ArrayList<>(List.of("banana", "apple", "cherry", "apple"));
list.size(); // 4(允许重复)
list.get(1); // "apple"
Set<String> set = new TreeSet<>(list);
set.size(); // 3(自动去重)
Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 3);
stock.put("apple", 4); // 覆盖,不是累加
stock.get("apple"); // 4Map<String, Integer> counter = new HashMap<>();
for (String word : List.of("java", "go", "java")) {
counter.merge(word, 1, Integer::sum); // 存在则 +1,不存在则置 1
}
// {go=1, java=2}merge 是计数场景的标准写法(《HashMap》 会展开)。
// 错误一:Arrays.asList 后试图添加元素
List<String> list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException
// 错误二:List.of 里放 null
List<String> withNull = List.of("a", null); // NullPointerException
// 错误三:遍历时修改集合
for (String item : list) {
if (item.equals("a")) {
list.remove(item); // ConcurrentModificationException
}
}第三个错误 《迭代器与集合遍历陷阱》 会专门讲——它有三种正确写法,而且"删倒数第二个元素时不会报错"这个巧合让 bug 更难发现。
文件:labs/collections/src/main/java/cn/kb/learn/collection/CollectionOverview.java
package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public class CollectionOverview {
public static void main(String[] args) {
// 1. List:有序、可重复、可按下标访问
List<String> list = new ArrayList<>(List.of("banana", "apple", "cherry", "apple"));
System.out.println("List 原样: " + list);
System.out.println("List 长度(含重复): " + list.size());
System.out.println("按下标访问: " + list.get(1));
Collections.sort(list);
System.out.println("排序后: " + list);
// 2. Set:自动去重
Set<String> set = new TreeSet<>(list);
System.out.println("Set 去重后(TreeSet 保证顺序): " + set);
System.out.println("Set 长度: " + set.size());
// 3. Map:键值对,key 唯一
Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 3);
stock.put("banana", 5);
stock.put("apple", 4); // 同一个 key 会覆盖
System.out.println("Map(TreeMap 保证顺序): " + new TreeMap<>(stock));
System.out.println("是否包含 apple: " + stock.containsKey("apple"));
System.out.println("不存在的 key 用默认值: " + stock.getOrDefault("cherry", 0));
// 4. 不可变集合
List<String> immutable = List.of("x", "y");
try {
immutable.add("z");
} catch (UnsupportedOperationException exception) {
System.out.println("List.of 创建的是不可变集合");
}
// 5. Map 的标准遍历方式
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("张三", 90);
scores.put("李四", 85);
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(" 遍历 entrySet: " + entry.getKey() + " -> " + entry.getValue());
}
// 6. 集合与数组互转
String[] array = list.toArray(new String[0]);
System.out.println("List 转数组: " + Arrays.toString(array));
System.out.println("数组转 List(固定长度): " + Arrays.asList(array));
System.out.println("数组转不可变 List: " + List.of(array));
}
}运行命令:
cd labs
./mvnw -pl collections compile
java -cp collections/target/classes cn.kb.learn.collection.CollectionOverview预期输出:
List 原样: [banana, apple, cherry, apple]
List 长度(含重复): 4
按下标访问: apple
排序后: [apple, apple, banana, cherry]
Set 去重后(TreeSet 保证顺序): [apple, banana, cherry]
Set 长度: 3
Map(TreeMap 保证顺序): {apple=4, banana=5}
是否包含 apple: true
不存在的 key 用默认值: 0
List.of 创建的是不可变集合
遍历 entrySet: 张三 -> 90
遍历 entrySet: 李四 -> 85
List 转数组: [apple, apple, banana, cherry]
数组转 List(固定长度): [apple, apple, banana, cherry]
数组转不可变 List: [apple, apple, banana, cherry]两处值得注意:
List 长度 4 vs Set 长度 3——重复的 apple 被 Set 去掉了;stock.put("apple", 4) 之后输出是 {apple=4, banana=5}——put 是覆盖语义,不是累加。想要累加要用 merge。set.size() 处断点,展开 set(TreeSet)观察内部是 TreeMap——TreeSet 底层就是一个 TreeMap,值是一个占位对象;map.getOrDefault(...) 处 F7 进入,观察源码里 ((v = get(key)) != null) ? v : defaultValue 的实现;immutable.add("z") 处断点,F7 进入后发现它直接 throw new UnsupportedOperationException()——立即失败,而不是静默忽略;list.toArray(new String[0]) 处展开 array,再对比 Arrays.asList(array) 的对象类型(是 Arrays$ArrayList 而不是 java.util.ArrayList)。Arrays.asList 之后想增删 List<String> list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException修复:需要可变时用 new ArrayList<>(Arrays.asList(...)) 或 new ArrayList<>(List.of(...))。
List.of 里放 null List.of("a", null); // NullPointerException修复:需要容纳 null 时用 Arrays.asList 或 new ArrayList<>()。
见 6.3 节。修复:改用 removeIf、迭代器的 remove、或倒序下标循环(《迭代器与集合遍历陷阱》 有完整对比)。
== 比较元素 if (list.get(0) == "apple") { } // 比较地址
if ("apple".equals(list.get(0))) { } // 正确keySet() + get() 遍历 Map 见 5.3 节。修复:用 entrySet()。
目标:用三种集合组合解决一个真实问题。
步骤:
List<String>,形如 "张三:90"),用 Map<String, Integer> 解析并保存;Set 找出所有不同的分数(去重后的分数集合);Map 做分数分布统计(分数段 → 人数);TreeMap 而不是 HashMap。验收方式:能说明每一步为什么选那种集合,以及换成别的集合会有什么问题。
扩展任务:把第 2 步的结果转成一个不可变 Map(Map.copyOf),并写一个方法返回它——体会"方法返回不可变集合"这个实践的好处。
基础题
Set 中的元素是有序的。(对 / 错)Map 应该用 ______;Arrays.asList 返回的 List 长度是 ______ 的。List.of 与 Arrays.asList 的区别是什么?代码阅读题
List<String> list = new ArrayList<>(List.of("a", "b", "a"));
Set<String> set = new HashSet<>(list);
Map<String, Integer> map = new HashMap<>();
for (String item : list) {
map.merge(item, 1, Integer::sum);
}
System.out.println(list.size() + " / " + set.size() + " / " + map.size());set.size() 与 map.size() 相同?编程题
Map<String, List<String>> groupByFirstLetter(List<String> words):按首字母分组(忽略大小写),返回的 Map 用 TreeMap 保证 key 有序,value 用 List。综合任务
Map<String, Integer> stock 保存商品与数量;void addStock(String sku, int quantity):增加库存;boolean takeStock(String sku, int quantity):扣减库存,库存不足返回 false 且不扣减;List<String> lowStock(int threshold):返回库存低于阈值的商品,按商品名排序;Map 而不是两个 List 存商品与数量?提示 1(方向):第 4 题的关键是 Set 去重与 Map 的 key 唯一性是同一个机制。
提示 2(关键点):第 5 题的 key 用 Character.toUpperCase(word.charAt(0)) 转成字符串;分组时用 computeIfAbsent(word, k -> new ArrayList<>()).add(...)。
提示 3(接近答案):第 6 题的 takeStock 要先判断 getOrDefault(sku, 0) >= quantity,再扣减——先检查后操作,避免出现负库存。
Set 的"有序"取决于实现:HashSet 不保证顺序,LinkedHashSet 保持插入顺序,TreeSet 按比较器排序。接口本身不承诺顺序。entrySet()、固定。List.of 返回完全不可变的集合(不能增删改,也不能放 null);Arrays.asList 返回长度固定但元素可修改(set 可以,add / remove 不行)的视图,它是数组的包装。需要可变时都要再包一层 new ArrayList<>(...)。输出 3 / 2 / 2。
list.size() = 3(允许重复的 "a");set.size() = 2(去重);map.size() = 2(key 唯一,"a" 出现两次但只对应一个 key,值为 2)。set.size() 与 map.size() 相同的原因是:HashSet 底层就是一个 HashMap——Set 的元素就是 Map 的 key,值用一个共享的占位对象填充。所以两者的"唯一性"机制完全一致(《HashMap》 会看到源码)。
package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class WordGrouper {
static Map<String, List<String>> groupByFirstLetter(List<String> words) {
Map<String, List<String>> groups = new TreeMap<>();
if (words == null) {
return groups;
}
for (String word : words) {
if (word == null || word.isBlank()) {
continue;
}
String key = String.valueOf(Character.toUpperCase(word.charAt(0)));
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(word);
}
return groups;
}
public static void main(String[] args) {
List<String> words = List.of("apple", "Banana", "avocado", "cherry", "blueberry");
Map<String, List<String>> groups = groupByFirstLetter(words);
groups.forEach((key, value) -> System.out.println(key + " -> " + value));
}
}预期输出(TreeMap 保证 key 有序):
A -> [apple, avocado]
B -> [Banana, blueberry]
C -> [cherry]package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class StockManager {
private final Map<String, Integer> stock = new TreeMap<>();
public void addStock(String sku, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须为正");
}
stock.merge(sku, quantity, Integer::sum);
}
public boolean takeStock(String sku, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须为正");
}
int current = stock.getOrDefault(sku, 0);
if (current < quantity) {
return false;
}
stock.put(sku, current - quantity);
return true;
}
public List<String> lowStock(int threshold) {
List<String> result = new ArrayList<>();
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
if (entry.getValue() < threshold) {
result.add(entry.getKey());
}
}
return result;
}
public static void main(String[] args) {
StockManager manager = new StockManager();
manager.addStock("A100", 5);
manager.addStock("B200", 2);
manager.addStock("C300", 10);
System.out.println("扣减成功: " + manager.takeStock("A100", 3));
System.out.println("库存不足: " + manager.takeStock("A100", 10));
System.out.println("低于阈值 5 的商品: " + manager.lowStock(5));
}
}为什么用 Map 而不是两个 List:
Map.get 是 O(1),两个 List 要按下标同步维护,查找要遍历;List 必须保证下标对应(容易错位);Map 的 key-value 天然绑定;Map<String, Integer> 直接表达"商品 → 数量",代码即文档。takeStock 的顺序很关键:先判断库存是否足够,再扣减。如果写成"先扣再判断",就要处理"回滚"——先检查后操作是避免这类复杂度的通用手法。
预期输出:
扣减成功: true
库存不足: false
低于阈值 5 的商品: [B200]entrySet() 遍历 Map,并知道为什么不推荐 keySet() + get();List.of / Arrays.asList 的边界,不会踩坑;《ArrayList 与 LinkedList》:同样实现 List,为什么一个适合随机访问、一个适合频繁插入删除。