Collections 工具类详解
本章定位:掌握
java.util.Collections工具类的全部核心静态方法——排序、查找、反转、打乱、统计、同步包装、不可变包装和类型安全包装。这些方法是操作集合的"瑞士军刀"。在飞翔科技的工资报表排序、季度数据打乱(模拟测试)、线程安全改造、防止堆污染等场景中广泛使用。本章由白歌系统讲解工具类设计思想,小崔用 Lambda 大幅简化代码,孔蓝发现了 unchecked 堆污染漏洞,Frank 和大翔在月度报告中使用 sort 和 binarySearch。
定义速览表
| 属性 | 说明 |
|---|---|
| 全类名 | java.util.Collections |
| 构造器 | private —— 禁止实例化(纯工具类) |
| 方法性质 | 全部为 public static 方法 |
| 设计模式 | 装饰器模式(synchronized/unmodifiable/checked 包装) |
| 排序算法 | List.sort() → TimSort(对象数组);Arrays.sort() → DualPivotQuickSort(基本类型数组) |
| 二分查找 | 基于索引(ArrayList)或迭代器(LinkedList),要求 List 已升序排列 |
| 线程安全包装 | synchronizedXxx() —— 方法级 synchronized,迭代需手动加锁 |
| 不可变包装 | unmodifiableXxx() —— 写操作抛 UnsupportedOperationException,浅不可变 |
| 与 Collection 区别 | Collection 是接口(定义集合通用操作);Collections 是工具类(操作集合的静态方法) |
方法分类速查表
排序与查找
| 方法 | 描述 |
|---|---|
sort(List<T>) | 按自然顺序排序(元素须实现 Comparable) |
sort(List<T>, Comparator<? super T>) | 按指定 Comparator 排序 |
binarySearch(List<? extends Comparable<? super T>>, T) | 二分查找(List 必须已升序排序) |
binarySearch(List<? extends T>, T, Comparator<? super T>) | 带 Comparator 的二分查找 |
reverse(List<?>) | 反转列表 |
shuffle(List<?>) | 随机打乱(使用默认随机源) |
shuffle(List<?>, Random) | 随机打乱(指定随机源,可复现) |
swap(List<?>, int, int) | 交换两个位置的元素 |
fill(List<? super T>, T) | 用指定元素填充整个列表 |
copy(List<? super T>, List<? extends T>) | 将源列表复制到目标列表(目标长度须 >= 源) |
rotate(List<?>, int distance) | 循环移动元素(正数右移、负数左移) |
replaceAll(List<T>, T oldVal, T newVal) | 替换列表中所有指定旧值为新值 |
极值、频率与统计
| 方法 | 描述 |
|---|---|
min(Collection<? extends T>) | 返回自然顺序下的最小元素 |
min(Collection<? extends T>, Comparator<? super T>) | 按指定 Comparator 返回最小 |
max(Collection<? extends T>) | 返回自然顺序下的最大元素 |
max(Collection<? extends T>, Comparator<? super T>) | 按指定 Comparator 返回最大 |
frequency(Collection<?>, Object) | 统计指定元素在集合中出现的次数 |
线程安全包装
| 方法 | 返回类型 | 说明 |
|---|---|---|
synchronizedCollection(Collection<T>) | Collection<T> | 线程安全的 Collection |
synchronizedList(List<T>) | List<T> | 线程安全的 List |
synchronizedSet(Set<T>) | Set<T> | 线程安全的 Set |
synchronizedSortedSet(SortedSet<T>) | SortedSet<T> | 线程安全的 SortedSet |
synchronizedMap(Map<K,V>) | Map<K,V> | 线程安全的 Map |
synchronizedSortedMap(SortedMap<K,V>) | SortedMap<K,V> | 线程安全的 SortedMap |
不可变包装
| 方法 | 返回类型 | 说明 |
|---|---|---|
unmodifiableCollection(Collection<? extends T>) | Collection<T> | 只读 Collection |
unmodifiableList(List<? extends T>) | List<T> | 只读 List |
unmodifiableSet(Set<? extends T>) | Set<T> | 只读 Set |
unmodifiableMap(Map<? extends K, ? extends V>) | Map<K,V> | 只读 Map |
unmodifiableSortedMap(SortedMap<K, ? extends V>) | SortedMap<K,V> | 只读 SortedMap |
类型安全包装
| 方法 | 说明 |
|---|---|
checkedCollection(Collection<E>, Class<E>) | 运行时检查元素类型,防止堆污染 |
checkedList(List<E>, Class<E>) | 运行时检查 List 元素类型 |
checkedSet(Set<E>, Class<E>) | 运行时检查 Set 元素类型 |
checkedMap(Map<K,V>, Class<K>, Class<V>) | 运行时检查 Map 键值类型 |
特殊集合与工具
| 方法 | 描述 |
|---|---|
emptyList() / emptySet() / emptyMap() | 返回不可变的空集合(全局单例共享) |
singleton(T) / singletonList(T) / singletonMap(K,V) | 返回仅含一个元素的不可变集合 |
nCopies(int n, T o) | 返回包含 n 个相同元素副本的不可变 List |
reverseOrder() | 返回自然顺序的逆序 Comparator |
reverseOrder(Comparator<T>) | 返回指定 Comparator 的逆序 |
newSetFromMap(Map<E,Boolean>) | 从 Map 创建 Set(用于 ConcurrentHashMap → ConcurrentHashSet) |
list(Enumeration<T>) | 将遗留 Enumeration 转为 ArrayList |
enumeration(Collection<T>) | 返回传统 Enumeration 适配器 |
disjoint(Collection<?>, Collection<?>) | 判断两个集合是否完全无交集 |
indexOfSubList(List<?>, List<?>) | 返回子列表首次出现位置 |
lastIndexOfSubList(List<?>, List<?>) | 返回子列表最后出现位置 |
核心原理
sort() 与排序算法选择
TimSort vs DualPivotQuickSort:
| 维度 | TimSort(对象排序) | DualPivotQuickSort(基本类型排序) |
|---|---|---|
| 算法类型 | 归并排序 + 插入排序(混合) | 双轴快速排序 |
| 时间复杂度 | 最好 O(n),最坏 O(n log n) | 平均 O(n log n) |
| 空间复杂度 | O(n) | O(log n) |
| 稳定性 | 稳定(相等元素相对顺序不变) | 不稳定 |
| 适用数据 | 部分有序数据极快 | 完全随机数据快 |
| 原因 | 对象排序需要稳定性(多次排序不破坏前一次顺序) | 基本类型无"同一性"概念,追求速度 |
binarySearch 的内部两种实现
关键:
binarySearch要求列表已按升序排序(使用与排序时相同的 Comparator),否则结果不可预测。返回值 < 0 时,-(index + 1)给出了元素"应插入位置"。
三种包装器的装饰器模式
三者都采用装饰器模式(Decorator Pattern):包装对象实现相同的接口,在方法调用前后添加额外逻辑(同步/禁止修改/类型检查),最终委托给原集合。
完整代码示例
示例一:飞翔科技月度工资报表 —— 排序与查找
import java.util.*;
/**
* 场景:月初,大翔要求对飞翔科技各部门提交的薪资报表进行排序和统计。
* 白歌使用 Collections 工具类完成排序、查找、反转和频率统计。
* Frank 要求报表能按部门索引快速定位特定工资。
*/
public class FeiXiangSalaryReport {
/** 员工工资条目 */
static class SalaryEntry implements Comparable<SalaryEntry> {
final String name;
final String department;
final double salary;
SalaryEntry(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// 自然排序:按工资升序(Comparable)
@Override
public int compareTo(SalaryEntry o) {
return Double.compare(this.salary, o.salary);
}
@Override
public String toString() {
return String.format("%s(%s, ¥%.0f)", name, department, salary);
}
}
public static void main(String[] args) {
// ========== 1. 原始工资数据 ==========
List<SalaryEntry> salaries = new ArrayList<>(Arrays.asList(
new SalaryEntry("大翔", "管理层", 80000),
new SalaryEntry("白歌", "技术部", 35000),
new SalaryEntry("小崔", "技术部", 12000),
new SalaryEntry("孔蓝", "测试部", 15000),
new SalaryEntry("Frank", "市场部", 22000),
new SalaryEntry("黄俪", "人事部", 18000),
new SalaryEntry("赵鸣", "技术部", 35000),
new SalaryEntry("孙鹤", "技术部", 12000)
));
System.out.println("=== 飞翔科技月度工资报表 ===\n");
System.out.println("原始数据: " + salaries.size() + " 条记录");
// ========== 2. sort:按自然顺序(升序) ==========
Collections.sort(salaries);
System.out.println("\n--- 按工资升序(自然排序) ---");
salaries.forEach(s -> System.out.println(" " + s));
// ========== 3. sort + reverseOrder:降序 ==========
Collections.sort(salaries, Collections.reverseOrder());
System.out.println("\n--- 按工资降序 ---");
salaries.forEach(s -> System.out.println(" " + s));
// 恢复升序以便后续操作
Collections.sort(salaries);
// ========== 4. binarySearch:二分查找 ==========
System.out.println("\n--- 二分查找(List 已按升序排列) ---");
SalaryEntry target = new SalaryEntry("", "", 22000);
int idx = Collections.binarySearch(salaries, target);
System.out.println("查找 ¥22000: 索引 = " + idx + " → " + salaries.get(idx));
// 查找不存在的工资
int idx2 = Collections.binarySearch(salaries, new SalaryEntry("", "", 20000));
System.out.println("查找 ¥20000: 索引 = " + idx2
+ " (不存在, 应插入位置: " + (-(idx2 + 1)) + ")");
// ========== 5. min / max ==========
SalaryEntry minSalary = Collections.min(salaries);
SalaryEntry maxSalary = Collections.max(salaries);
System.out.println("\n--- 极值 ---");
System.out.println("最低工资: " + minSalary);
System.out.println("最高工资: " + maxSalary);
// ========== 6. frequency:频率统计 ==========
SalaryEntry query = new SalaryEntry("", "", 12000);
int freq = Collections.frequency(salaries, query);
System.out.println("\n--- 频率统计 ---");
System.out.println("工资恰好为 ¥12000 的人数: " + freq);
// ========== 7. reverse:反转 ==========
System.out.println("\n--- 反转列表 ---");
Collections.reverse(salaries);
System.out.println("反转后前三: " + salaries.subList(0, 3));
// 恢复升序
Collections.sort(salaries);
// ========== 8. shuffle + rotate ==========
System.out.println("\n--- 随机打乱 + 循环移动 ---");
// 使用固定种子确保演示可复现
Collections.shuffle(salaries, new Random(42));
System.out.println("打乱后: " + salaries.subList(0, 5) + "...");
Collections.rotate(salaries, 3); // 右移 3 位
System.out.println("右移3位后: " + salaries.subList(0, 5) + "...");
// ========== 9. 按部门排序(自定义 Comparator) ==========
System.out.println("\n--- 按部门 + 姓名排序 ---");
Collections.sort(salaries, Comparator
.comparing((SalaryEntry s) -> s.department)
.thenComparing(s -> s.name));
salaries.forEach(s -> System.out.println(" " + s));
}
}
=== 飞翔科技月度工资报表 ===
原始数据: 8 条记录
--- 按工资升序(自然排序) ---
小崔(技术部, ¥12000)
孙鹤(技术部, ¥12000)
孔蓝(测试部, ¥15000)
黄俪(人事部, ¥18000)
Frank(市场部, ¥22000)
白歌(技术部, ¥35000)
赵鸣(技术部, ¥35000)
大翔(管理层, ¥80000)
--- 按工资降序 ---
大翔(管理层, ¥80000)
白歌(技术部, ¥35000)
赵鸣(技术部, ¥35000)
Frank(市场部, ¥22000)
黄俪(人事部, ¥18000)
孔蓝(测试部, ¥15000)
小崔(技术部, ¥12000)
孙鹤(技术部, ¥12000)
--- 二分查找(List 已按升序排列) ---
查找 ¥22000: 索引 = 4 → Frank(市场部, ¥22000)
查找 ¥20000: 索引 = -5 (不存在, 应插入位置: 4)
--- 极值 ---
最低工资: 小崔(技术部, ¥12000)
最高工资: 大翔(管理层, ¥80000)
--- 频率统计 ---
工资恰好为 ¥12000 的人数: 2
--- 反转列表 ---
反转后前三: [大翔(管理层, ¥80000), 赵鸣(技术部, ¥35000), 白歌(技术部, ¥35000)]
--- 随机打乱 + 循环移动 ---
打乱后: [赵鸣(技术部, ¥35000), 孔蓝(测试部, ¥15000), Frank(市场部, ¥22000), 大翔(管理层, ¥80000), 白歌(技术部, ¥35000)]...
右移3位后: [黄俪(人事部, ¥18000), 小崔(技术部, ¥12000), 赵鸣(技术部, ¥35000), 孔蓝(测试部, ¥15000), Frank(市场部, ¥22000)]...
--- 按部门 + 姓名排序 ---
大翔(管理层, ¥80000)
Frank(市场部, ¥22000)
黄俪(人事部, ¥18000)
孔蓝(测试部, ¥15000)
白歌(技术部, ¥35000)
孙鹤(技术部, ¥12000)
小崔(技术部, ¥12000)
赵鸣(技术部, ¥35000)
示例二:线程安全包装与不可变包装 —— 白歌的安全改造
import java.util.*;
/**
* 场景:飞翔科技的共享配置数据被多个线程同时访问。
* 白歌要求小崔对配置列表进行线程安全改造,并对只读配置进行不可变保护。
* 孔蓝负责验证线程安全性和不可变性的边界情况。
*/
public class FeiXiangCollectionsWrapper {
/** 模拟飞翔科技的在线用户列表(多线程共享) */
static class OnlineUserTracker {
// 线程安全的 List
private final List<String> onlineUsers;
public OnlineUserTracker() {
// synchronizedList 包装普通的 ArrayList
this.onlineUsers = Collections.synchronizedList(new ArrayList<>());
}
/** 用户上线 */
public void login(String username) {
onlineUsers.add(username);
}
/** 用户下线 */
public void logout(String username) {
onlineUsers.remove(username);
}
/** 获取在线用户快照(安全遍历) */
public List<String> getOnlineUserSnapshot() {
// 遍历时必须手动加锁!
synchronized (onlineUsers) {
return new ArrayList<>(onlineUsers); // 返回副本
}
}
/** 获取在线人数 */
public int getOnlineCount() {
return onlineUsers.size();
}
}
/** 飞翔科技的系统配置(只读) */
static class SystemConfig {
private final List<String> adminNames;
private final Map<String, String> settings;
public SystemConfig() {
// 内部使用可变的集合
List<String> admins = new ArrayList<>();
admins.add("大翔");
admins.add("白歌");
Map<String, String> configs = new HashMap<>();
configs.put("company", "飞翔科技");
configs.put("version", "2.0.0");
configs.put("maxLoginAttempts", "5");
// 暴露不可变视图
this.adminNames = Collections.unmodifiableList(admins);
this.settings = Collections.unmodifiableMap(configs);
}
public List<String> getAdminNames() { return adminNames; }
public Map<String, String> getSettings() { return settings; }
}
public static void main(String[] args) {
// ========== 一、线程安全包装 ==========
System.out.println("=== 线程安全包装演示 ===");
OnlineUserTracker tracker = new OnlineUserTracker();
// 模拟多用户上线
tracker.login("大翔");
tracker.login("白歌");
tracker.login("小崔");
tracker.login("孔蓝");
System.out.println("当前在线: " + tracker.getOnlineUserSnapshot());
System.out.println("在线人数: " + tracker.getOnlineCount());
tracker.logout("小崔");
System.out.println("小崔下线后: " + tracker.getOnlineUserSnapshot());
// 验证线程安全:多线程同时操作
System.out.println("\n--- 多线程并发登录测试 ---");
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
tracker.login("用户A-" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
tracker.login("用户B-" + i);
}
});
t1.start(); t2.start();
try { t1.join(); t2.join(); } catch (InterruptedException e) {}
System.out.println("并发操作后在线人数: " + tracker.getOnlineCount()
+ " (期望: " + (4 - 1 + 200) + ")");
// ========== 二、不可变包装 ==========
System.out.println("\n=== 不可变包装演示 ===");
SystemConfig config = new SystemConfig();
System.out.println("管理员列表: " + config.getAdminNames());
System.out.println("系统配置: " + config.getSettings());
// 尝试修改不可变列表
try {
config.getAdminNames().add("Frank"); // 应该抛异常
} catch (UnsupportedOperationException e) {
System.out.println("修改被拦截: " + e.getClass().getSimpleName()
+ " —— 不可变集合不允许修改");
}
// 读取依然正常
System.out.println("管理员列表(仍为只读): " + config.getAdminNames());
// ========== 三、特殊集合 ==========
System.out.println("\n=== 特殊集合 ===");
// 空集合(共享单例)
List<String> emptyList = Collections.emptyList();
System.out.println("空列表: " + emptyList + " (isEmpty: " + emptyList.isEmpty() + ")");
// 单元素集合(不可变)
Set<String> ceo = Collections.singleton("大翔-CEO");
System.out.println("CEO 集合: " + ceo);
// nCopies
List<String> pendingStatus = Collections.nCopies(5, "待审批");
System.out.println("5 个待审批: " + pendingStatus);
// ========== 四、Comparator 工具方法 ==========
System.out.println("\n=== Comparator 工具 ===");
List<String> names = new ArrayList<>(Arrays.asList(
"白歌", "小崔", "大翔", "孔蓝", "Frank"
));
// 自然顺序的逆序
Collections.sort(names, Collections.reverseOrder());
System.out.println("逆字典序: " + names);
// 恢复自然顺序
Collections.sort(names);
System.out.println("自然字典序: " + names);
// ========== 五、disjoint:判断无交集 ==========
System.out.println("\n=== 集合运算 ===");
Set<String> techDept = new HashSet<>(Arrays.asList("白歌", "小崔", "赵鸣", "孙鹤"));
Set<String> mgmtDept = new HashSet<>(Arrays.asList("大翔", "Frank"));
Set<String> allDept = new HashSet<>(Arrays.asList("白歌", "小崔", "大翔", "Frank", "孔蓝"));
System.out.println("技术部与管理层无交集? "
+ Collections.disjoint(techDept, mgmtDept)); // true
System.out.println("技术部与全员有交集? "
+ Collections.disjoint(techDept, allDept)); // false
}
}
=== 线程安全包装演示 ===
当前在线: [大翔, 白歌, 小崔, 孔蓝]
在线人数: 4
小崔下线后: [大翔, 白歌, 孔蓝]
--- 多线程并发登录测试 ---
并发操作后在线人数: 203 (期望: 203)
=== 不可变包装演示 ===
管理员列表: [大翔, 白歌]
系统配置: {version=2.0.0, company=飞翔科技, maxLoginAttempts=5}
修改被拦截: UnsupportedOperationException —— 不可变集合不允许修改
管理员列表(仍为只读): [大翔, 白歌]
=== 特殊集合 ===
空列表: [] (isEmpty: true)
CEO 集合: [大翔-CEO]
5 个待审批: [待审批, 待审批, 待审批, 待审批, 待审批]
=== Comparator 工具 ===
逆字典序: [白歌, 小崔, 孔蓝, 大翔, Frank]
自然字典序: [Frank, 小崔, 大翔, 孔蓝, 白歌]
=== 集合运算 ===
技术部与管理层无交集? true
技术部与全员有交集? false
易错场景
反例一:未排序就调用 binarySearch
小崔第一次使用二分查找时,忘了先排序:
// ❌ 错误:未排序的 list 上调用 binarySearch,结果不可预测
List<Integer> list = Arrays.asList(5, 2, 8, 1, 3);
int idx = Collections.binarySearch(list, 2);
System.out.println(idx); // 可能是任何值(正数/负数),完全不可靠!
纠正:
// ✅ 正确:先排序再二分查找
Collections.sort(list); // [1, 2, 3, 5, 8]
int idx = Collections.binarySearch(list, 2); // 1,正确
反例二:synchronizedList 遍历时未手动加锁
孔蓝发现小崔在多线程遍历时没有手动同步:
// ❌ 错误:遍历 synchronizedList 时未手动同步
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("大翔"); syncList.add("白歌"); syncList.add("小崔");
// 线程1:用 for-each 遍历(内部使用迭代器)
new Thread(() -> {
for (String s : syncList) {
System.out.println(s); // 可能抛 ConcurrentModificationException
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}).start();
// 线程2:添加
new Thread(() -> {
syncList.add("孔蓝"); // 虽然 add 是同步的,但无法保护整个迭代过程
}).start();
纠正:
// ✅ 正确:遍历时手动加锁
synchronized (syncList) { // 锁住整个遍历过程
for (String s : syncList) {
System.out.println(s);
}
}
// ✅ 或使用迭代器 + 手动加锁
synchronized (syncList) {
Iterator<String> it = syncList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
反例三:unmodifiableList 是浅不可变,元素内部状态仍可修改
// ❌ 误区:以为 unmodifiableList 连元素状态都保护了
class MutableEmployee {
String name;
MutableEmployee(String name) { this.name = name; }
void setName(String name) { this.name = name; }
@Override
public String toString() { return name; }
}
List<MutableEmployee> list = new ArrayList<>();
list.add(new MutableEmployee("小崔"));
list.add(new MutableEmployee("白歌"));
List<MutableEmployee> unmodList = Collections.unmodifiableList(list);
// 不能修改 List 结构(抛异常)
// unmodList.add(new MutableEmployee("孔蓝")); // ✗ UnsupportedOperationException
// 但可以修改元素内部状态!
unmodList.get(0).setName("小崔(已修改)"); // ✓ 成功了!
System.out.println(unmodList); // [小崔(已修改), 白歌]
纠正:
// ✅ 如需深度不可变,元素类也应该是不可变的
class ImmutableEmployee {
private final String name;
public ImmutableEmployee(String name) { this.name = name; }
public String getName() { return name; }
// 无 setter 方法 —— 完全不可变
}
// ✅ 或使用 Guava 的 ImmutableList.copyOf(深拷贝)
// ImmutableList<Employee> immutable = ImmutableList.copyOf(original);
反例四:copy 方法目标列表不够长
// ❌ 错误:copy 要求目标列表长度 >= 源列表长度
List<String> src = Arrays.asList("A", "B", "C", "D");
List<String> dest = new ArrayList<>(2); // 长度不够!
Collections.copy(dest, src); // IndexOutOfBoundsException!
纠正:
// ✅ 方案一:预先填充够长度的元素
List<String> dest = new ArrayList<>(Collections.nCopies(src.size(), ""));
Collections.copy(dest, src); // OK
// ✅ 方案二:直接用构造器(推荐)
List<String> dest = new ArrayList<>(src); // 更简单直接
面试考点
Q1:Collections 和 Collection 的区别?
Collection是集合框架的顶层接口(java.util.Collection),定义了add、remove、contains等集合通用操作,List 和 Set 都继承自它。Collections是工具类(java.util.Collections),全部由静态方法组成(private 构造器),提供排序、查找、线程安全包装、不可变包装等操作集合的方法。两者的关系是"接口 vs 工具类",没有继承关系——一个是"数据结构是什么",一个是"我能对数据结构做什么"。
Q2:Collections.sort 底层使用什么排序算法?为什么对象和基本类型用不同算法?
JDK 8 中
Collections.sort(list)委托给List.sort(),底层对对象数组使用 TimSort(自适应混合排序:归并 + 插入,稳定排序,最坏 O(n log n),最好 O(n))。对基本类型数组(通过Arrays.sort()),使用 DualPivotQuickSort(双轴快速排序,不稳定排序)。原因:对象排序需要稳定性——当用户先按部门再按工资排序时,稳定排序保证同一部门内工资顺序不变。基本类型没有"同一性"概念(1 就是 1,没有两个不同的 1),不需要稳定性,因此使用速度更快但不稳定的快排。
Q3:Collections.synchronizedList 和 Vector 的区别?synchronizedList 和 CopyOnWriteArrayList 如何选择?
维度 synchronizedList Vector CopyOnWriteArrayList 实现方式 装饰器模式(包装任意 List) 自身是 List 实现(数组) 写时复制(新数组) 出现时间 JDK 1.2 JDK 1.0 JDK 1.5 迭代加锁 需要手动加锁 需要(或 Enumeration) 不需要(快照) 读性能 需竞争锁 需竞争锁 无锁,极高 写性能 需竞争锁 需竞争锁 极差(复制整个数组) 适用场景 读写均衡 不推荐(遗留类) 读多写少 读多写少(如配置信息、黑名单)→
CopyOnWriteArrayList;读写均衡 →Collections.synchronizedList或ConcurrentLinkedQueue;新项目不推荐 Vector。
Q4:unmodifiableList 和真正的不可变集合有什么不同?
unmodifiableList返回的是原集合的只读视图: ① 浅不可变:只禁止修改 List 结构(add/remove/set),不保护元素内部状态(仍可通过get(i).setXxx()修改元素字段); ② 非独立:如果原集合被修改(通过原始引用),unmodifiableList 视图会反映变化——它只是原集合的一个"查看窗口"; ③ 非深拷贝:不复制元素本身。真正的不可变集合(如 Guava 的
ImmutableList): ① 深拷贝:创建时复制所有元素,与原集合完全独立; ② 深不可变:通常要求元素类也是不可变的; ③ Java 9+ 提供了List.of()/Set.of()工厂方法创建真正的不可变集合。在 JDK 8 中,如果需要真正的不可变性,应使用
new ArrayList<>(original)复制一份后用unmodifiableList包装,并确保元素类不可变。
Q5:Collections.emptyList() 有什么优势?为什么不用 new ArrayList<>() 返回空列表?
emptyList()返回的是一个不可变的空列表单例——所有调用共享同一个实例。优势: ① 零内存分配:全局只有一个空列表对象,多次调用不产生新对象; ② 线程安全:不可变对象天然线程安全; ③ 明确语义:这是一个"故意为空的列表"而非"忘了填充的列表"; ④ 防御性编程:调用方无法修改返回的空列表(抛 UnsupportedOperationException),避免了意外修改。在生产代码中,当方法返回"无结果"时,返回
Collections.emptyList()比返回null更好——调用方无需做 null 检查,可以直接使用 for-each 遍历(空遍历不会抛 NPE)。
Q6:Collections.checkedList 解决了什么问题?
checkedList在运行时提供类型安全检查,防止泛型擦除导致的"堆污染"(heap pollution)。由于 Java 的泛型信息在编译后会被擦除,可以通过原始类型(raw type)绕过编译期检查:List<String> list = new ArrayList<>(); List raw = list; // 原始类型 raw.add(123); // 放入 Integer,编译不报错! String s = list.get(0); // ClassCastException 在运行时才暴露使用
CheckedList包装后:List<String> list = Collections.checkedList(new ArrayList<>(), String.class);,即使在运行时通过原始类型绕过编译检查,add方法也会调用type.isInstance(e)进行类型验证,在插入时立即抛出异常(fail-fast),而不是等后续读取时才暴露。