Collections、可变参数与集合综合应用 | JavaSE

Collections、可变参数与集合综合应用

一、学习目标

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

  1. 能够准确区分 CollectionCollectionsArrays 三个概念。
  2. 能够使用 Collections 工具类完成批量添加、打乱、排序、查找、反转、交换、最大最小值和频次统计等常见操作。
  3. 能够解释什么是可变参数(Variable Arity Parameter / Varargs),并手写 int... nums 等方法。
  4. 能够解释为什么可变参数本质上以数组形式进入方法内部。
  5. 能够掌握可变参数“最多一个且必须位于形参列表最后”的语法规则。
  6. 能够综合比较 List、Set、Map 各主要实现类并完成合理技术选型。
  7. 能够综合使用 ArrayList、HashSet、TreeSet、HashMap、LinkedHashMap、TreeMap 与 Collections 解决实际问题。

二、核心知识

2.1 Collection 和 Collections 完全不是一回事

这是一个非常容易混淆的知识点。

前面学习的:

java.util.Collection

是:

单列集合体系的根接口之一。

结构类似:

Collection<E>
├── List<E>
└── Set<E>

而:

java.util.Collections

是:

集合工具类。

它里面提供了大量:

static

工具方法。

例如:

Collections.addAll(...);
Collections.shuffle(...);
Collections.sort(...);

因此:

Collection
没有 s
→ 接口
→ 集合体系

Collections
有 s
→ 工具类
→ 操作集合

这是必须彻底区分的一组名称。


2.2 Collections 为什么存在

假设:

List<String> names = new ArrayList<>();

List 自己已经拥有:

add
remove
get
set
contains
...

那么 Collections 为什么还存在?

因为很多能力并不是:

“某一个 List 对象独有的基本操作”。

而属于:

通用集合算法。

例如:

把 List 打乱
给 List 排序
交换两个位置
反转整个 List
查找最大值
统计元素出现次数

这些通用能力集中到:

Collections

中。

可以理解为:

ArrayList
→ 数据结构

Collections
→ 操作集合的工具箱

2.3 Collections 的典型特点

Collections 中大量方法都是:

public static

因此不需要:

new Collections()

而是直接:

Collections.xxx(...)

例如:

Collections.shuffle(list);

这与之前学习的:

Arrays.sort(array);

思想非常相似。


三、使用方法

3.1 Collections.addAll:批量添加

普通写法:

List<String> names = new ArrayList<>();

names.add("张三");
names.add("李四");
names.add("王五");
names.add("赵六");

可以改成:

Collections.addAll(
        names,
        "张三",
        "李四",
        "王五",
        "赵六"
);

方法思想:

Collections.addAll(collection, element1, element2, ...);

第一个参数:

目标 Collection

后面:

任意数量的待添加元素

这实际上已经使用到了本章另一个知识:

可变参数

3.2 Collections.shuffle:打乱 List

例如:

List<String> cards = new ArrayList<>();

Collections.addAll(
        cards,
        "A", "2", "3", "4", "5"
);

Collections.shuffle(cards);

它会重新排列 List 中的元素。

经典场景:

扑克牌洗牌
题目随机顺序
抽奖候选顺序
随机展示内容

注意:

shuffle 操作的是 List

因为“打乱”需要处理:

元素的位置顺序

而 HashSet 本身没有稳定的位置语义。


3.3 Collections.sort:自然排序

例如:

List<Integer> numbers =
        new ArrayList<>(List.of(5, 2, 8, 1, 3));

Collections.sort(numbers);

System.out.println(numbers);

结果按照 Integer 的自然顺序排列:

1
2
3
5
8

要求元素具有可比较能力。

例如:

Integer
String

本身已经实现了 Comparable。


3.4 Collections.sort + Comparator

如果排序的是自定义对象:

List<Student> students = new ArrayList<>();

可以:

Collections.sort(
        students,
        Comparator.comparingInt(Student::getScore)
);

成绩降序:

Collections.sort(
        students,
        Comparator.comparingInt(Student::getScore)
                .reversed()
);

不过在现代 Java 中,也常直接使用:

students.sort(
        Comparator.comparingInt(Student::getScore)
);

二者都应该认识。


3.5 reverse:反转 List

例如:

List<Integer> numbers =
        new ArrayList<>(List.of(1, 2, 3, 4, 5));

Collections.reverse(numbers);

结果:

5
4
3
2
1

注意:

reverse

和:

降序排序

不是一个概念。

假设原列表:

3 1 2

reverse:

2 1 3

而降序排序:

3 2 1

所以:

reverse 只是把“当前位置顺序”倒过来,并不关心元素大小。


3.6 swap:交换两个元素

例如:

List<String> list =
        new ArrayList<>(List.of("A", "B", "C"));

Collections.swap(list, 0, 2);

变成:

C
B
A

本质上相当于:

交换两个索引位置上的元素

3.7 max 与 min

例如:

List<Integer> scores =
        List.of(80, 95, 72, 100, 63);

int max = Collections.max(scores);
int min = Collections.min(scores);

获得:

最大元素
最小元素

默认根据:

自然排序

也可以提供 Comparator:

Student best =
        Collections.max(
                students,
                Comparator.comparingInt(Student::getScore)
        );

3.8 frequency:统计元素出现次数

例如:

List<String> words =
        List.of(
                "Java",
                "Python",
                "Java",
                "C",
                "Java"
        );

int count =
        Collections.frequency(words, "Java");

System.out.println(count);

结果:

3

内部判断“相同元素”的语义基于对象相等关系。

这种 API 适合:

一次性查询某个元素出现次数

但是如果需要统计:

所有不同单词各出现多少次

那么使用:

HashMap<String,Integer>

一次遍历统计通常更合理。


3.9 binarySearch:二分查找

例如:

List<Integer> numbers =
        new ArrayList<>(List.of(10, 20, 30, 40, 50));

int index =
        Collections.binarySearch(numbers, 30);

返回目标索引。

但有一个极其重要的前提:

List 必须已经按照与 binarySearch 一致的规则排好序。

正确流程:

先 sort
↓
再 binarySearch

不能:

乱序 List
↓
直接 binarySearch

否则结果没有可靠意义。

例如:

Collections.sort(numbers);

int index =
        Collections.binarySearch(numbers, 30);

四、原理与进阶

4.1 什么是可变参数

观察:

public static void sum(int... nums) {

}

这里:

int... nums

就是:

可变参数(Varargs)。

它解决的问题是:

一个方法需要接收相同类型、数量不确定的一组参数。

调用时:

sum();

可以零个参数。

也可以:

sum(10);

还可以:

sum(10, 20);

甚至:

sum(10, 20, 30, 40, 50);

因此:

可变参数
=
调用方传多少个都比较灵活

4.2 完整案例

public class VarargsDemo {

    public static void main(String[] args) {
        sum();
        sum(10);
        sum(10, 20);
        sum(10, 20, 30, 40);

        int[] numbers = {1, 2, 3, 4};
        sum(numbers);
    }

    public static void sum(int... nums) {
        int result = 0;

        for (int num : nums) {
            result += num;
        }

        System.out.println(result);
    }
}

调用形式非常灵活。


4.3 可变参数在方法内部是什么

在方法内部:

int... nums

可以像数组一样使用。

例如:

nums.length
nums[0]

以及:

for (int num : nums) {

}

因此初学阶段可以建立:

对调用者:
可以传 0~N 个参数

对方法内部:
nums 以数组形式处理

这就是可变参数的核心模型。


4.4 为什么可以传数组

既然方法内部本质上处理:

int[]

那么:

int[] arr = {1, 2, 3};

sum(arr);

也是合法调用方式。

所以:

sum(1, 2, 3);

与:

sum(new int[]{1, 2, 3});

都可以使用。


4.5 一个方法最多只能有一个可变参数

下面不允许:

public static void test(
        int... nums,
        String... names
) {

}

因为调用:

test(...)

时编译器将无法合理确定:

哪些参数属于 nums?

哪些参数属于 names?

所以一个方法:

最多只能声明一个可变参数。


4.6 可变参数必须放在最后

正确:

public static void register(
        String username,
        String... roles
) {

}

调用:

register(
        "LingXi",
        "USER",
        "ADMIN"
);

编译器知道:

第一个参数
→ username

剩下全部
→ roles

但是:

public static void register(
        String... roles,
        String username
) {

}

是不合法的。

所以规则:

一个方法:
最多一个 varargs

并且:
varargs 必须位于形参列表最后

这两条规则必须记住。


4.7 Collections.addAll 为什么能接受很多参数

观察其使用:

Collections.addAll(
        list,
        "Java",
        "C",
        "Python",
        "Go"
);

从思维上可以理解成:

addAll(Collection<T> collection, T... elements)

这正是:

泛型
+
可变参数

组合使用的典型例子。

于是以前学过的几个知识开始真正连接起来:

泛型
  ↓
T

可变参数
  ↓
T...

集合
  ↓
Collection<? super T>

Java API 并不是一个个孤立知识点。

它们会逐渐组合起来。


4.8 泛型可变参数的高级问题

对于普通:

int... nums

理解相对简单。

但如果出现:

T... elements

就涉及:

泛型擦除
+
数组运行时类型
+
堆污染 Heap Pollution

等问题。

因此你以后会看到一些 API:

@SafeVarargs

例如:

Collections.addAll(...)

就使用了这类机制。

现阶段不要求深入掌握堆污染细节,但需要建立意识:

T... 比普通 int... 涉及更多泛型运行时安全问题。

后续遇到 @SafeVarargs 时不要觉得它是“神秘注解”。


五、实践应用

5.1 斗地主:集合知识第一次完整组合

斗地主是一个非常典型的集合综合案例。

可以把整个问题拆成:

54 张牌
↓
List<Card>

洗牌
↓
Collections.shuffle

发牌
↓
List 操作

玩家与手牌
↓
Map<String,List<Card>>

手牌排序
↓
Comparator / List.sort

这里已经综合使用:

类与对象
泛型
List
Map
Comparator
Collections
循环
Random/随机思想

这就是“集合综合应用”的真正意义:

不再只问某个 API 怎么写,而是问一个业务应该如何选择和组合集合。


5.2 集合选型第一原则:先看数据模型

遇到需求时,第一问题不是:

ArrayList 还是 HashMap?

而应该先问:

数据之间是什么关系?

如果:

一组独立对象

首先考虑:

Collection

如果:

key → value
一一映射

首先考虑:

Map

5.3 List 的选择

如果要求:

允许重复
需要索引
保留顺序

考虑:

List

其中:

ArrayList

通常是默认选择。

特点:

数组结构
随机访问快
综合使用频率高

而:

LinkedList

底层双向链表,在某些首尾操作、Deque 场景更合适。

不要看到:

“链表插入 O(1)”

就机械认为:

“只要插入多就一定用 LinkedList。”

因为实际业务还涉及:

  • 定位插入位置的成本
  • CPU cache locality
  • 内存开销
  • 随机访问需求

集合选型应结合完整访问模式。


5.4 Set 的选择

如果:

不允许重复

考虑 Set。

HashSet

不要求顺序
+
去重
+
快速查找

优先:

HashSet

LinkedHashSet

去重
+
需要确定迭代顺序

考虑:

LinkedHashSet

TreeSet

去重
+
自动排序

考虑:

TreeSet

5.5 Map 的选择

如果数据模型:

key → value

考虑 Map。

HashMap

普通映射
+
不要求顺序
+
高频增删查

通常首选:

HashMap

LinkedHashMap

Map
+
需要确定的迭代顺序

考虑:

LinkedHashMap

TreeMap

Map
+
key 自动排序
+
区间查询

考虑:

TreeMap

5.6 集合框架完整选择模型

可以形成以下决策树:

需要存什么?

├── 单列数据
│
│   ├── 允许重复?
│   │
│   │   ├── 是
│   │   │    ↓
│   │   │   List
│   │   │   ├── ArrayList
│   │   │   └── LinkedList
│   │   │
│   │   └── 否
│   │        ↓
│   │       Set
│   │       ├── HashSet
│   │       ├── LinkedHashSet
│   │       └── TreeSet
│
└── key → value
     ↓
    Map
    ├── HashMap
    ├── LinkedHashMap
    └── TreeMap

第二步再问:

需要索引吗?
需要去重吗?
需要保留顺序吗?
需要自动排序吗?
需要范围查询吗?
主要操作是什么?

5.7 一张表总结集合框架

| 集合 | 重复 | 索引 | 顺序特征 | 自动排序 | 主要结构 | | ------------- | ---------- | ------------ | ------------ | -------- | ------------- | | ArrayList | 可以 | 有 | 插入顺序 | 否 | 动态数组 | | LinkedList | 可以 | 有 List 语义 | 插入顺序 | 否 | 双向链表 | | HashSet | 不可以 | 无 | 不保证 | 否 | 哈希表 | | LinkedHashSet | 不可以 | 无 | 维护确定顺序 | 否 | 哈希表 + 链表 | | TreeSet | 不可以 | 无 | 比较顺序 | 是 | 红黑树 | | HashMap | key 不重复 | 无 | 不保证 | 否 | 哈希表 | | LinkedHashMap | key 不重复 | 无 | 维护确定顺序 | 否 | 哈希表 + 链表 | | TreeMap | key 不重复 | 无 | key 比较顺序 | 是 | 红黑树 |

这张表不是为了死背。

真正目的:

看到业务需求时能够自动映射到合适的数据结构。


六、常见问题

6.1 Collection 和 Collections 有什么区别?

Collection
→ 接口
→ 单列集合体系

Collections
→ 工具类
→ 提供集合算法

6.2 Collections.sort 和 List.sort 有什么区别?

现代 Java 中二者都可以排序 List。

例如:

Collections.sort(list, comparator);

以及:

list.sort(comparator);

都应该认识。

实际代码中:

list.sort(...)

通常更加面向对象、直接。

但 Collections 仍然是必须掌握的重要工具类。


6.3 shuffle 可以保证绝对密码学随机吗?

不能把它理解为密码学随机 API。

它的目标是:

随机排列 List 元素。

适合:

洗牌
随机展示
普通随机顺序

安全令牌、密码学密钥等问题属于另一类安全随机需求。


6.4 binarySearch 为什么必须先排序?

因为二分查找的前提就是:

数据已经有序

它利用:

中间元素

判断:

目标在左边
还是右边

如果数据无序,这种推理基础直接消失。


6.5 int... numsint[] nums 完全一样吗?

对方法内部而言都可以通过数组方式处理。

但是调用体验不同。

数组参数通常要求:

sum(new int[]{1, 2, 3});

而可变参数允许:

sum(1, 2, 3);

所以 varargs 的核心价值在于:

改善调用方的参数传递体验。


6.6 可变参数可以没有参数吗?

可以。

例如:

sum();

此时方法内部相当于得到:

长度为 0 的数组

6.7 为什么可变参数必须放最后?

因为编译器必须明确:

前面的固定参数属于谁
剩余参数全部属于谁

如果可变参数放在中间:

参数边界无法正常确定

所以 Java 语法直接禁止。


6.8 为什么不能所有场景都使用 ArrayList?

因为不同需求需要不同的数据结构。

例如:

用户 ID 去重

ArrayList 可以做:

遍历 + contains

但:

HashSet

更直接表达“唯一集合”语义。

又例如:

id → Student

使用:

HashMap<Integer,Student>

比每次遍历 List 找 ID 更符合模型。

技术选型首先应该正确表达:

数据关系和访问模式。


6.9 为什么 HashMap 是 Map 中最常见的默认选择?

因为大量业务只需要:

key → value
+
快速查找

并不需要:

插入顺序
自动排序
范围导航

此时 HashMap 通常足够。

只有存在明确顺序需求时,再考虑:

LinkedHashMap
TreeMap

七、练习与验收

7.1 知识问答

  1. CollectionCollections 有什么区别?
  2. Collections 为什么被称为工具类?
  3. Collections.addAll 解决什么问题?
  4. shuffle 适用于什么类型的集合?
  5. sort 的自然排序依赖什么?
  6. 如何使用 Comparator 自定义排序?
  7. reverse 和降序排序有什么本质区别?
  8. frequency 解决什么问题?
  9. binarySearch 的必要前提是什么?
  10. 什么是可变参数?
  11. 可变参数在方法内部如何表现?
  12. 可变参数为什么可以直接传数组?
  13. 一个方法最多可以有几个可变参数?
  14. 可变参数为什么必须位于最后?
  15. ArrayList、HashSet、HashMap 各自适合解决什么核心问题?
  16. LinkedHashSet 与 HashSet 的关键区别是什么?
  17. TreeSet 和 TreeMap 分别排序什么?
  18. LinkedHashMap 与 TreeMap 的“有序”有什么区别?

7.2 代码阅读

阅读:

List<Integer> list = new ArrayList<>();

Collections.addAll(
        list,
        30, 10, 20, 40
);

Collections.sort(list);
Collections.reverse(list);

System.out.println(list);

回答:

  1. addAll 后是什么顺序?
  2. sort 后是什么顺序?
  3. reverse 后是什么顺序?
  4. reverse 是否执行了“降序排序算法”?

阅读:

public static void print(String prefix, int... nums) {
    System.out.println(nums.length);
}

调用:

print("A");
print("B", 10);
print("C", 10, 20, 30);

回答:

  1. 三次调用的 nums.length 分别是多少?
  2. prefix 属于可变参数吗?
  3. 为什么 int... nums 必须放在最后?

7.3 手写代码

  1. 创建一个 List,使用 Collections.addAll 一次加入 10 个整数。
  2. 使用 shuffle 打乱。
  3. 使用 sort 升序。
  4. 使用 Comparator 降序。
  5. 使用 max/min 获取最大最小值。
  6. 使用 frequency 统计指定元素出现次数。
  7. 使用 binarySearch 查找指定数字。
  8. 手写:
sum(int... nums)

完成任意数量整数求和。

  1. 手写:
max(int... nums)

求任意数量整数最大值,并考虑零参数时应该如何设计。

7.4 Debug

下面代码是否合法?

public static void test(
        int... nums,
        String name
) {

}

要求:

  1. 判断编译结果。
  2. 说明错误规则。
  3. 修改为正确参数列表。

下面代码:

List<Integer> list =
        new ArrayList<>(
                List.of(50, 10, 40, 20, 30)
        );

int index =
        Collections.binarySearch(list, 40);

要求:

  1. 找出逻辑问题。
  2. 说明为什么二分查找依赖排序。
  3. 修改程序。

下面代码:

Set<String> set = new HashSet<>();

Collections.shuffle(set);

要求:

  1. 判断能否编译。
  2. 分析 shuffle 为什么针对 List。
  3. 如果业务确实需要把一组 Set 数据随机排列,应该如何转换?

7.5 综合训练

综合任务一:扑克牌系统

设计:

Card

包含:

花色
点数
排序权重

要求:

  1. 使用 List 保存 54 张扑克牌。
  2. 使用 Collections.shuffle 洗牌。
  3. 给三名玩家发牌。
  4. 使用 Map<String,List<Card>> 保存玩家和手牌。
  5. 对每名玩家手牌排序。
  6. 输出三名玩家最终手牌。

综合任务二:学生管理数据结构设计

系统需要:

Student
id
name
score

分别设计:

  1. 所有学生按录入顺序保存。
  2. 保证用户名不能重复。
  3. 根据 studentId 快速找到 Student。
  4. 按 studentId 升序输出。
  5. 按 score 排行。

要求:

每一项都必须说明为什么选择相应集合,而不仅仅写出代码。


综合任务三:集合框架最终选型

下面需求分别选择集合:

  1. 保存购物车商品,允许相同商品多次出现。
  2. 保存系统权限代码,不能重复,不关心顺序。
  3. 保存搜索历史,要求去重且保留访问顺序。
  4. 保存唯一成绩,并自动从小到大排序。
  5. 根据 userId 快速找到 User。
  6. 根据配置 key 保存 value,并保持写入顺序。
  7. 保存“分数线 → 等级”,并执行区间查询。

要求对每一道写出:

集合
+
底层结构
+
选择理由

7.6 本章验收

完成本章后,应能够闭卷做到:

  • [ ] 10 秒内说出 Collection 与 Collections 的区别。
  • [ ] 独立使用 addAll、shuffle、sort。
  • [ ] 知道 reverse、swap、max、min、frequency、binarySearch 的用途。
  • [ ] 手写一个 int... nums 方法。
  • [ ] 解释 varargs 为什么在内部表现为数组。
  • [ ] 说出“最多一个、必须最后”两条规则。
  • [ ] 画出 List、Set、Map 的完整主要体系。
  • [ ] 说出八种主要集合实现的核心区别。
  • [ ] 面对一个业务需求能够说明为什么选择某个集合。
  • [ ] 不借助资料完成 List、Set、Map 的基本创建、添加和遍历。
  • [ ] 能够综合使用多个集合完成一个小型业务问题。

完成这些能力后,才算真正结束 Java 集合框架基础阶段。