Collections、可变参数与集合综合应用 | JavaSE
Collections、可变参数与集合综合应用
一、学习目标
完成本章后,你应该能够:
- 能够准确区分
Collection、Collections与Arrays三个概念。 - 能够使用
Collections工具类完成批量添加、打乱、排序、查找、反转、交换、最大最小值和频次统计等常见操作。 - 能够解释什么是可变参数(Variable Arity Parameter / Varargs),并手写
int... nums等方法。 - 能够解释为什么可变参数本质上以数组形式进入方法内部。
- 能够掌握可变参数“最多一个且必须位于形参列表最后”的语法规则。
- 能够综合比较 List、Set、Map 各主要实现类并完成合理技术选型。
- 能够综合使用 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... nums 和 int[] 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 知识问答
Collection和Collections有什么区别?- Collections 为什么被称为工具类?
Collections.addAll解决什么问题?shuffle适用于什么类型的集合?sort的自然排序依赖什么?- 如何使用 Comparator 自定义排序?
reverse和降序排序有什么本质区别?frequency解决什么问题?binarySearch的必要前提是什么?- 什么是可变参数?
- 可变参数在方法内部如何表现?
- 可变参数为什么可以直接传数组?
- 一个方法最多可以有几个可变参数?
- 可变参数为什么必须位于最后?
- ArrayList、HashSet、HashMap 各自适合解决什么核心问题?
- LinkedHashSet 与 HashSet 的关键区别是什么?
- TreeSet 和 TreeMap 分别排序什么?
- 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);
回答:
addAll后是什么顺序?sort后是什么顺序?reverse后是什么顺序?- reverse 是否执行了“降序排序算法”?
阅读:
public static void print(String prefix, int... nums) {
System.out.println(nums.length);
}
调用:
print("A");
print("B", 10);
print("C", 10, 20, 30);
回答:
- 三次调用的
nums.length分别是多少? prefix属于可变参数吗?- 为什么
int... nums必须放在最后?
7.3 手写代码
- 创建一个 List,使用
Collections.addAll一次加入 10 个整数。 - 使用
shuffle打乱。 - 使用
sort升序。 - 使用 Comparator 降序。
- 使用
max/min获取最大最小值。 - 使用
frequency统计指定元素出现次数。 - 使用
binarySearch查找指定数字。 - 手写:
sum(int... nums)
完成任意数量整数求和。
- 手写:
max(int... nums)
求任意数量整数最大值,并考虑零参数时应该如何设计。
7.4 Debug
下面代码是否合法?
public static void test(
int... nums,
String name
) {
}
要求:
- 判断编译结果。
- 说明错误规则。
- 修改为正确参数列表。
下面代码:
List<Integer> list =
new ArrayList<>(
List.of(50, 10, 40, 20, 30)
);
int index =
Collections.binarySearch(list, 40);
要求:
- 找出逻辑问题。
- 说明为什么二分查找依赖排序。
- 修改程序。
下面代码:
Set<String> set = new HashSet<>();
Collections.shuffle(set);
要求:
- 判断能否编译。
- 分析
shuffle为什么针对 List。 - 如果业务确实需要把一组 Set 数据随机排列,应该如何转换?
7.5 综合训练
综合任务一:扑克牌系统
设计:
Card
包含:
花色
点数
排序权重
要求:
- 使用 List 保存 54 张扑克牌。
- 使用
Collections.shuffle洗牌。 - 给三名玩家发牌。
- 使用
Map<String,List<Card>>保存玩家和手牌。 - 对每名玩家手牌排序。
- 输出三名玩家最终手牌。
综合任务二:学生管理数据结构设计
系统需要:
Student
id
name
score
分别设计:
- 所有学生按录入顺序保存。
- 保证用户名不能重复。
- 根据 studentId 快速找到 Student。
- 按 studentId 升序输出。
- 按 score 排行。
要求:
每一项都必须说明为什么选择相应集合,而不仅仅写出代码。
综合任务三:集合框架最终选型
下面需求分别选择集合:
- 保存购物车商品,允许相同商品多次出现。
- 保存系统权限代码,不能重复,不关心顺序。
- 保存搜索历史,要求去重且保留访问顺序。
- 保存唯一成绩,并自动从小到大排序。
- 根据 userId 快速找到 User。
- 根据配置 key 保存 value,并保持写入顺序。
- 保存“分数线 → 等级”,并执行区间查询。
要求对每一道写出:
集合
+
底层结构
+
选择理由
7.6 本章验收
完成本章后,应能够闭卷做到:
- [ ] 10 秒内说出 Collection 与 Collections 的区别。
- [ ] 独立使用 addAll、shuffle、sort。
- [ ] 知道 reverse、swap、max、min、frequency、binarySearch 的用途。
- [ ] 手写一个
int... nums方法。 - [ ] 解释 varargs 为什么在内部表现为数组。
- [ ] 说出“最多一个、必须最后”两条规则。
- [ ] 画出 List、Set、Map 的完整主要体系。
- [ ] 说出八种主要集合实现的核心区别。
- [ ] 面对一个业务需求能够说明为什么选择某个集合。
- [ ] 不借助资料完成 List、Set、Map 的基本创建、添加和遍历。
- [ ] 能够综合使用多个集合完成一个小型业务问题。
完成这些能力后,才算真正结束 Java 集合框架基础阶段。