Skip to content

List、Set、Map ​

S1-015必学入门约 90 分钟JDK 21编程语言基础 · 集合

1. 本节定位 ​

《数组》 的结尾留了三个问题:长度固定、无法去重、无法表达"键 → 值"的映射。集合框架解决它们。

本节是三节集合课的总览:先建立"什么场景用什么集合"的判断力,再在后续两节深入具体实现(《ArrayList 与 LinkedList》、《HashMap》)。

2. 学习目标 ​

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

  • 说清 List、Set、Map 三者的语义差别,并做对选型;
  • 熟练使用增删改查、判空、遍历等常用操作;
  • 用 List.of / Set.of / Map.of 创建不可变集合,并知道它们的边界;
  • 用 entrySet() 正确遍历 Map,并说清为什么不用 keySet() 再 get;
  • 认出 Arrays.asList、遍历中修改、== 比较元素这三类高频错误。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

数组能解决"多个同类型数据",但实际需求往往更复杂:

需求数组集合
动态添加元素需要自己扩容复制add 即可
自动去重自己写双重循环Set 一行
按 key 查找遍历比对Map.get
判空、包含、批量操作手写循环现成 API

集合的选择直接决定代码复杂度。选错集合的典型症状:明明该用 Map,却写了三层循环去查找。

5. 核心概念 ​

5.1 三大接口的语义 ​

text
Collection
 ├── List:有序、可重复、可按下标访问        (列表)
 └── Set :不重复                          (集合)

Map :键值对,key 唯一(不属于 Collection)  (映射)
接口是否有序是否允许重复访问方式典型用途
List是(插入序)是下标 get(i)有序数据、结果集
Set取决于实现否遍历去重、存在性判断
Map取决于实现key 唯一key 查找缓存、计数、映射

一句话选型

  • 需要顺序和重复 → List
  • 需要去重或只关心存在性 → Set
  • 需要按 key 找值 → Map

5.2 常用操作 ​

java
List<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 的对应操作:

java
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");

5.3 遍历 Map 的正确姿势 ​

java
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());
}

不推荐:

java
for (String key : scores.keySet()) {
    Integer value = scores.get(key);     // 多一次查找
}

keySet() 再 get() 会对每个 key 做一次额外查找。数据量大时差距明显——遍历 Map 就用 entrySet()。

Map 还有两个视图:keySet()(所有 key 的集合)与 values()(所有 value 的集合),它们都是视图,不是副本,改视图会影响原 Map。

5.4 不可变集合 ​

java
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);   // 创建当时的快照

三条边界:

  1. 不能增删改——调用 add 抛 UnsupportedOperationException;
  2. 不能放 null——List.of("x", null) 直接抛 NullPointerException;
  3. Set.of / Map.of 不允许重复元素(重复直接抛异常)。

适用场景:常量数据、方法返回值、传给别人的只读快照。这是 《封装》 里"防御性拷贝"的标准工具。

5.5 三种遍历方式 ​

java
// 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));

5.6 集合与数组互转 ​

java
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);           // 数组 → 不可变 List

Arrays.asList 的坑:返回的 List 长度固定,可以 set 但不能 add / remove——因为它只是数组的包装,不是真正的 ArrayList。

6. 递进示例 ​

6.1 最小示例:三种集合的基本用法 ​

java
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");              // 4

6.2 业务示例:用 Map 做计数 ​

java
Map<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》 会展开)。

6.3 反例:三个高频错误 ​

java
// 错误一: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 更难发现。

7. 完整可运行代码 ​

文件:labs/collections/src/main/java/cn/kb/learn/collection/CollectionOverview.java

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));
    }
}

运行命令:

powershell
cd labs
./mvnw -pl collections compile
java -cp collections/target/classes cn.kb.learn.collection.CollectionOverview

预期输出:

text
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]

两处值得注意:

  1. List 长度 4 vs Set 长度 3——重复的 apple 被 Set 去掉了;
  2. stock.put("apple", 4) 之后输出是 {apple=4, banana=5}——put 是覆盖语义,不是累加。想要累加要用 merge。

8. Debug 观察点 ​

  1. 在 set.size() 处断点,展开 set(TreeSet)观察内部是 TreeMap——TreeSet 底层就是一个 TreeMap,值是一个占位对象;
  2. 在 map.getOrDefault(...) 处 F7 进入,观察源码里 ((v = get(key)) != null) ? v : defaultValue 的实现;
  3. 在 immutable.add("z") 处断点,F7 进入后发现它直接 throw new UnsupportedOperationException()——立即失败,而不是静默忽略;
  4. 在 list.toArray(new String[0]) 处展开 array,再对比 Arrays.asList(array) 的对象类型(是 Arrays$ArrayList 而不是 java.util.ArrayList)。

9. 常见错误 ​

错误 1:Arrays.asList 之后想增删 ​

java
List<String> list = Arrays.asList("a", "b");
list.add("c");                   // UnsupportedOperationException

修复:需要可变时用 new ArrayList<>(Arrays.asList(...)) 或 new ArrayList<>(List.of(...))。

错误 2:List.of 里放 null ​

java
List.of("a", null);              // NullPointerException

修复:需要容纳 null 时用 Arrays.asList 或 new ArrayList<>()。

错误 3:遍历时修改集合 ​

见 6.3 节。修复:改用 removeIf、迭代器的 remove、或倒序下标循环(《迭代器与集合遍历陷阱》 有完整对比)。

错误 4:用 == 比较元素 ​

java
if (list.get(0) == "apple") { }        // 比较地址
if ("apple".equals(list.get(0))) { }   // 正确

错误 5:keySet() + get() 遍历 Map ​

见 5.3 节。修复:用 entrySet()。

10. 动手实验 ​

目标:用三种集合组合解决一个真实问题。

步骤:

  1. 运行第 7 节代码,核对输出;
  2. 给一组学生成绩(List<String>,形如 "张三:90"),用 Map<String, Integer> 解析并保存;
  3. 用 Set 找出所有不同的分数(去重后的分数集合);
  4. 用 Map 做分数分布统计(分数段 → 人数);
  5. 把结果按分数段排序输出,并说明你为什么选 TreeMap 而不是 HashMap。

验收方式:能说明每一步为什么选那种集合,以及换成别的集合会有什么问题。

扩展任务:把第 2 步的结果转成一个不可变 Map(Map.copyOf),并写一个方法返回它——体会"方法返回不可变集合"这个实践的好处。

11. 练习题 ​

基础题

  1. 判断:Set 中的元素是有序的。(对 / 错)
  2. 填空:遍历 Map 应该用 ______;Arrays.asList 返回的 List 长度是 ______ 的。
  3. 简答:List.of 与 Arrays.asList 的区别是什么?

代码阅读题

java
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());
  1. 输出是什么?为什么 set.size() 与 map.size() 相同?

编程题

  1. 写方法 Map<String, List<String>> groupByFirstLetter(List<String> words):按首字母分组(忽略大小写),返回的 Map 用 TreeMap 保证 key 有序,value 用 List。

综合任务

  1. 实现一个"库存管理"小系统:
    • 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,再扣减——先检查后操作,避免出现负库存。

12. 参考答案 ​

基础题
  1. 错。Set 的"有序"取决于实现:HashSet 不保证顺序,LinkedHashSet 保持插入顺序,TreeSet 按比较器排序。接口本身不承诺顺序。
  2. entrySet()、固定。
  3. List.of 返回完全不可变的集合(不能增删改,也不能放 null);Arrays.asList 返回长度固定但元素可修改(set 可以,add / remove 不行)的视图,它是数组的包装。需要可变时都要再包一层 new ArrayList<>(...)。
代码阅读题(第 4 题)

输出 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》 会看到源码)。

编程题(第 5 题)
java
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 有序):

text
A -> [apple, avocado]
B -> [Banana, blueberry]
C -> [cherry]
综合任务(第 6 题)
java
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:

  1. 查找效率:Map.get 是 O(1),两个 List 要按下标同步维护,查找要遍历;
  2. 一致性:两个 List 必须保证下标对应(容易错位);Map 的 key-value 天然绑定;
  3. 语义清晰:Map<String, Integer> 直接表达"商品 → 数量",代码即文档。

takeStock 的顺序很关键:先判断库存是否足够,再扣减。如果写成"先扣再判断",就要处理"回滚"——先检查后操作是避免这类复杂度的通用手法。

预期输出:

text
扣减成功: true
库存不足: false
低于阈值 5 的商品: [B200]

13. 掌握标准 ​

  • [ ] 能根据"是否需要顺序/去重/按 key 查找"选对集合;
  • [ ] 熟练使用三个集合的常用 API;
  • [ ] 会用 entrySet() 遍历 Map,并知道为什么不推荐 keySet() + get();
  • [ ] 知道 List.of / Arrays.asList 的边界,不会踩坑;
  • [ ] 遇到遍历中修改的异常时,知道有三种正确写法。

14. 扩展资源 ​

15. 下一步学习 ​

《ArrayList 与 LinkedList》:同样实现 List,为什么一个适合随机访问、一个适合频繁插入删除。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引