Appearance
Collections工具类
前置知识
在阅读本章前,你需要了解: Java基本集合框架(List、Set、Map),以及Java泛型的基础知识。
为什么需要 Collections 工具类?
想象一下,你手上有一筐水果,需要把苹果挑出来,或者把水果从大到小排个序。是不是有点麻烦?在Java世界里,操作集合数据时,我们也经常需要做类似的事情——找、排序、同步访问,还有构造不能改的“只读”集合。
自己写循环、写排序算法,不仅麻烦,而且容易出错。Java的 Collections 类就像一把瑞士军刀,帮你轻松完成这些常见操作,省了许多麻烦。不仅如此,它还提供了线程安全的包装功能,以及丰富的算法接口,帮你写出高效又安全的代码。
这章我们就来“拆开”这把瑞士军刀,逐块认识它的强大功能,从最简单的排序和查找说起,直到不可变集合和同步包装,帮你全面掌握 Collections 类。
排序与查找:Collections.sort 和 Collections.binarySearch
排序和查找是最基础也是最常用的操作。自己实现排序算法是锻炼编程能力,不过在项目中,我们往往要用标准且高效的方案。
什么是排序?
排序就是把集合中的元素按一定规则排列,比如数字升序,或字符串按字母序排列。Collections.sort() 是工具类中最常用的方法,内部用的是归并排序,时间复杂度优良。
为什么需要排序?
很多场景都要求有序数据,比如排行榜、待办事项列表、配置项顺序等。排序之后,我们还能用二分查找超快找到目标元素。
基础用法示例
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SortingExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Banana");
fruits.add("Apple");
fruits.add("Cherry");
System.out.println("排序前: " + fruits);
Collections.sort(fruits); // 默认按字母顺序排序
System.out.println("排序后: " + fruits);
// 使用 Collections.binarySearch 查找元素位置
int index = Collections.binarySearch(fruits, "Apple");
System.out.println("Apple 的索引: " + index);
}
}这段代码做了什么:
- 创建一个装水果名字的列表,依次加入香蕉、苹果、樱桃。
- 打印排序前列表顺序。
- 调用
Collections.sort()按字典序排序。 - 打印排序后的列表。
- 使用二分查找查找“Apple”的位置,返回索引。
注意,使用
Collections.binarySearch前一定要保证列表是有序的,否则结果会不可预测。
同步包装:Collections.synchronizedXXX
在多线程环境下,多个线程访问同一个集合容易引发竞争和数据不一致。最简单的保障方式是将集合包装成同步版本。
什么是同步包装?
同步包装就是让普通集合变成线程安全的集合。在Java中,Collections提供一系列 synchronizedList(), synchronizedSet() 等方法帮你“包裹”原集合,保证同步。
为什么需要同步包装?
假设你有一个共享的 ArrayList,两个线程同时对它添加元素,会导致程序崩溃甚至数据丢失。用同步包装的集合,则会自动加锁保证同步。
示例代码:同步包装
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedCollectionExample {
public static void main(String[] args) {
List<String> unsafeList = new ArrayList<>();
List<String> safeList = Collections.synchronizedList(unsafeList);
// 线程1
Thread t1 = new Thread(() -> {
synchronized (safeList) { // 在迭代时还需要显示加锁
safeList.add("Apple");
safeList.add("Banana");
System.out.println("线程1添加完成");
}
});
// 线程2
Thread t2 = new Thread(() -> {
synchronized (safeList) {
safeList.add("Cherry");
System.out.println("线程2添加完成");
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 迭代集合时,也需要加锁,避免并发修改异常
synchronized (safeList) {
System.out.println("集合内容: " + safeList);
}
}
}这段代码做了什么:
- 创建一个普通的
ArrayList,然后用Collections.synchronizedList包装成同步列表。 - 创建两个线程分别往列表添加元素,每个线程的添加操作都用
synchronized加锁。 - 主线程等待两个子线程执行完毕。
- 迭代打印列表时,也必须加锁保护。
注意,虽然包装后的集合是线程安全的,但迭代时仍需手动同步,否则可能出现并发修改异常。
不可变集合及算法方法
Java 还有一类非常实用的功能,就是不可变集合和丰富的算法。
什么是不可变集合?
不可变集合是指创建后不能修改的集合。它保证了数据安全性和线程安全,防止误操作导致数据变化。
Java 9+ 开始, Collections 提供了 emptyList(), singletonList(), 以及 unmodifiableList() 这类方法来创建只读集合。
为什么需要不可变集合?
不可变集合能防止别人意外改动你的集合,比如把配置或者缓存暴露给外部调用者,不希望他们随意改动。
实现不可变集合示例
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableCollectionExample {
public static void main(String[] args) {
List<String> modifiableList = new ArrayList<>();
modifiableList.add("Apple");
modifiableList.add("Banana");
List<String> unmodifiableList = Collections.unmodifiableList(modifiableList);
System.out.println("不可变集合内容: " + unmodifiableList);
// 下面这行会抛异常,因为集合是不可变的
// unmodifiableList.add("Cherry");
}
}这段代码做了什么:
- 创建一个可变的列表,加入两个元素。
- 通过
Collections.unmodifiableList创建不可变视图。 - 打印不可变集合内容。
- 注释了一个会抛出
UnsupportedOperationException的修改操作,提醒你不可修改。
Collections 中的算法方法概览
除了排序和查找,Collections 还有其他算法方法,比如:
reverse(List<?>):反转列表shuffle(List<?>):随机打乱列表顺序,适合做抽奖等fill(List<?>, T):用同一个元素替换列表所有元素copy(List<? super T>, List<? extends T>):复制一个列表到另一个已分配空间的列表中max和min:获取集合中的最大或最小元素
这些方法帮我们秒杀日常集合“搬砖”任务。
复杂示例:综合运用 Collections 算法
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ComplexAlgorithmExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
numbers.add(i);
}
System.out.println("原始数字: " + numbers);
// 反转
Collections.reverse(numbers);
System.out.println("反转后: " + numbers);
// 打乱顺序
Collections.shuffle(numbers);
System.out.println("打乱后: " + numbers);
// 填充所有元素为 0
Collections.fill(numbers, 0);
System.out.println("填充后: " + numbers);
// 复制示例 - 准备目标列表
List<Integer> destination = new ArrayList<>(Collections.nCopies(numbers.size(), -1));
Collections.copy(destination, numbers);
System.out.println("复制后的列表: " + destination);
}
}这段代码做了什么:
- 创建并初始化包含1至10的数字列表。
- 对列表进行反转操作。
- 随机打乱列表顺序。
- 将列表所有元素替换成0。
- 创建目标列表(大小一致),使用
Collections.copy复制内容。
⚠️ 常见陷阱
- binarySearch必须基于排序后的集合使用,否则结果不准。
- 使用
Collections.synchronizedXXX包装集合后,迭代时必须手动加锁,否则并发迭代会抛异常。 Collections.unmodifiableXXX返回的集合是“视图”,如果原集合被修改,不可变集合也会变化。Collections.copy需要目标列表大小至少与源列表一样大,否则抛IndexOutOfBoundsException。
💡 实战建议
- 在多线程环境,如果需要频繁读写,考虑使用
ConcurrentHashMap或CopyOnWriteArrayList替代同步包装集合。 - 优先使用 Java 9 及以上版本的
List.of(...)、Set.of(...)来创建不可变集合,代码更简洁且性能更优。 - 对于大型集合排序,优先用
Collections.sort,底层进行了优化而且线程安全性依赖调用环境。 - 使用
Collections.binarySearch之前,记得明确排序规则,要么是自然排序,要么传入自定义Comparator。
小结
Collections.sort和binarySearch是排序和查找的利器,前提是列表有序。- 同步包装
Collections.synchronizedList能快速解决简单多线程访问安全问题,但迭代时务必加锁。 Collections.unmodifiableList提供不可变集合视图,防止意外改动数据,适合暴露给外部调用。Collections还内置了丰富的算法方法,帮你快速完成反转、打乱、填充、复制等多种操作。
延伸思考
如果有自己的排序规则(比如自定义对象),你会如何将 Collections.sort 与 Comparator 结合使用?多线程环境下,除了同步包装,还有哪些更优雅的线程安全集合方案?你如何设计一个不可变集合的深拷贝来保证完全不变性?
代码归档
示例1:排序与查找
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SortingExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Banana");
fruits.add("Apple");
fruits.add("Cherry");
System.out.println("排序前: " + fruits);
Collections.sort(fruits); // 默认按字母顺序排序
System.out.println("排序后: " + fruits);
// 使用 Collections.binarySearch 查找元素位置
int index = Collections.binarySearch(fruits, "Apple");
System.out.println("Apple 的索引: " + index);
}
}示例2:同步包装
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedCollectionExample {
public static void main(String[] args) {
List<String> unsafeList = new ArrayList<>();
List<String> safeList = Collections.synchronizedList(unsafeList);
// 线程1
Thread t1 = new Thread(() -> {
synchronized (safeList) { // 在迭代时还需要显示加锁
safeList.add("Apple");
safeList.add("Banana");
System.out.println("线程1添加完成");
}
});
// 线程2
Thread t2 = new Thread(() -> {
synchronized (safeList) {
safeList.add("Cherry");
System.out.println("线程2添加完成");
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (safeList) {
System.out.println("集合内容: " + safeList);
}
}
}示例3:不可变集合与算法综合应用
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableCollectionExample {
public static void main(String[] args) {
List<String> modifiableList = new ArrayList<>();
modifiableList.add("Apple");
modifiableList.add("Banana");
List<String> unmodifiableList = Collections.unmodifiableList(modifiableList);
System.out.println("不可变集合内容: " + unmodifiableList);
// 下面这行会抛异常,因为集合是不可变的
// unmodifiableList.add("Cherry");
}
}
class ComplexAlgorithmExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
numbers.add(i);
}
System.out.println("原始数字: " + numbers);
Collections.reverse(numbers);
System.out.println("反转后: " + numbers);
Collections.shuffle(numbers);
System.out.println("打乱后: " + numbers);
Collections.fill(numbers, 0);
System.out.println("填充后: " + numbers);
List<Integer> destination = new ArrayList<>(Collections.nCopies(numbers.size(), -1));
Collections.copy(destination, numbers);
System.out.println("复制后的列表: " + destination);
}
}