集合排序与不可变集合
学习目标
- 用 Comparator.comparing、thenComparing 和 reversed 组合排序规则。
- 区分原地排序、排序副本、不可变集合和只读包装。
- 知道同步包装、并发集合和不可变集合分别解决什么问题。
核心知识点
专业术语
- Comparator:定义两个对象相对顺序的比较器。
- 稳定排序:比较结果相等时保留原有相对顺序。
- 不可变集合:创建后不能通过任何 API 改变容器结构。
- 只读视图:当前引用不能修改,但源集合变化仍会反映出来。
- 并发集合:为多线程访问提供特定的线程安全语义,不等于业务事务。
白话解释与边界
list.sort 和 Collections.sort 都是原地排序;如果不想改原列表,先复制再排序。List.of、Set.of、Map.of 和 copyOf 是不可修改结果,Collections.unmodifiableXxx 是只读包装视图。并发集合解决多个线程对容器的协调访问,不会让跨多个容器或多个步骤的业务逻辑自动原子化。
常用用法
Comparator.comparing:按对象字段排序
comparingInt、comparingLong、comparingDouble 避免手动拆箱比较;字符串等引用字段可使用 Comparator.comparing。
// 语义:comparingInt、comparingLong、comparingDouble 避免手动拆箱比较。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record User(String name, int age) {}
public class ComparingDemo {
public static void main(String[] args) {
List<User> users = new ArrayList<>(List.of(
new User("Bob", 30), new User("Alice", 20)));
// 两个 User 元素分别为 Bob/30 和 Alice/20;外层 List 按该顺序初始化。
users.sort(Comparator.comparingInt(User::age));
// 关键变化:users.sort(Comparator.comparingInt(User::age));;按 Comparator.comparingInt(User::age) 重排 users,返回 void;集合顺序更新。
System.out.println(users);
// 输出:[User[name=Alice, age=20], User[name=Bob, age=30]]
}
}thenComparing:增加次级排序
先比较主键,主键相等时再比较次键;先调用 reversed 再 thenComparing 可让主键降序而次键保持自然顺序。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Score(String name, int value) {}
public class ThenComparingDemo {
public static void main(String[] args) {
List<Score> scores = new ArrayList<>(List.of(
new Score("Bob", 90), new Score("Alice", 90)));
// 两个 Score 元素分别为 Bob/90 和 Alice/90;外层 List 按该顺序初始化。
scores.sort(Comparator.comparingInt(Score::value)
.reversed()
.thenComparing(Score::name));
// 关键变化:scores.sort(Comparator.comparingInt(Score::value) .reversed() .thenComparing(Score::name));;按 Comparator.comparingInt(Score::value) .reversed() .thenComparing(Score::name) 重排 scores,返回 void;集合顺序更新。
// 输入:.reversed() .thenComparing(Score::name));;当前 Comparator 先调用无参 reversed,再调用 thenComparing(Score::name)。
// 关键变化:.reversed() .thenComparing(Score::name));;先将按 value 的 Comparator 反向,再追加 Score::name 次级比较;返回组合 Comparator。
// 关键变化:.thenComparing(Score::name));;为当前 Comparator 增加 Score::name 次级比较;返回组合 Comparator。
System.out.println(scores);
// 输出:[Score[name=Alice, value=90], Score[name=Bob, value=90]]
}
}Comparator.nullsFirst:把空值排在前面
nullsFirst 用包装比较器明确把 null 放在所有非空值之前。
// 语义:nullsFirst 用包装比较器明确把 null 放在所有非空值之前。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class NullsFirstDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("b", null, "a"));
list.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
// 关键变化:list.sort(Comparator.nullsFirst(Comparator.naturalOrder()));;按 Comparator.nullsFirst(Comparator.naturalOrder()) 重排 list,返回 void;集合顺序更新。
System.out.println(list);
// 输出:[null, a, b]
}
}Comparator.nullsLast:把空值排在后面
nullsLast 用包装比较器明确把 null 放在所有非空值之后。
// 语义:nullsLast 用包装比较器明确把 null 放在所有非空值之后。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class NullsLastDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("b", null, "a"));
list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
// 关键变化:list.sort(Comparator.nullsLast(Comparator.naturalOrder()));;按 Comparator.nullsLast(Comparator.naturalOrder()) 重排 list,返回 void;集合顺序更新。
System.out.println(list);
// 输出:[a, b, null]
}
}reversed:反转比较器
Comparator.reverseOrder 或 comparator.reversed 都可以表达降序;不要在比较器中返回 value1 - value2,整数溢出会造成错误顺序。
// 语义:Comparator.reverseOrder 或 comparator.reversed 都可以表达降序。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ReversedComparatorDemo {
public static void main(String[] args) {
List<Integer> values = new ArrayList<>(List.of(1, 3, 2));
values.sort(Comparator.reverseOrder());
// 关键变化:values.sort(Comparator.reverseOrder());;按 Comparator.reverseOrder() 重排 values,返回 void;集合顺序更新。
System.out.println(values);
// 输出:[3, 2, 1]
}
}list.sort:原地排序
list.sort 会改变当前可变列表;List.of 返回的列表排序会抛 UnsupportedOperationException。
// 语义:list.sort 会改变当前可变列表。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ListSortOverviewDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("java", "c", "python"));
names.sort(Comparator.comparingInt(String::length));
// 关键变化:names.sort(Comparator.comparingInt(String::length));;按 Comparator.comparingInt(String::length) 重排 names,返回 void;集合顺序更新。
System.out.println(names);
// 输出:[c, java, python]
}
}复制后排序:保留原集合
需要保留调用方列表时复制到 ArrayList;排序结果也可以用 Stream.sorted().toList() 得到不可修改结果。
// 语义:需要保留调用方列表时复制到 ArrayList。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SortedCopyDemo {
public static void main(String[] args) {
List<Integer> source = List.of(3, 1, 2);
List<Integer> sorted = new ArrayList<>(source);
sorted.sort(Comparator.naturalOrder());
// 关键变化:sorted.sort(Comparator.naturalOrder());;按 Comparator.naturalOrder() 重排 sorted,返回 void;集合顺序更新。
System.out.println(source + " / " + sorted);
// 输出:[3, 1, 2] / [1, 2, 3]
}
}List.of:创建小型不可变列表
List.of 返回拒绝 null 的不可修改列表,并保留参数顺序。
// 语义:List.of 返回拒绝 null 的不可修改列表,并保留参数顺序。
import java.util.List;
public class ListOfDemo {
public static void main(String[] args) {
List<String> list = List.of("a", "b");
System.out.println(list);
// 输出:[a, b]
}
}Set.of:创建小型不可变集合
Set.of 返回拒绝 null 和重复元素的不可修改集合,遍历顺序不属于契约。
// 语义:Set.of 返回拒绝 null 和重复元素的不可修改集合,遍历顺序不属于契约。
import java.util.Set;
public class SetOfDemo {
public static void main(String[] args) {
Set<String> set = Set.of("java", "sql");
System.out.println(set.contains("java"));
// 输出:true
System.out.println(set.size());
// 输出:2
}
}Map.of:创建小型不可变映射
Map.of 返回拒绝 null 和重复键的不可修改映射。
// 语义:Map.of 返回拒绝 null 和重复键的不可修改映射。
import java.util.Map;
public class MapOfDemo {
public static void main(String[] args) {
Map<String, Integer> map = Map.of("java", 95, "sql", 88);
System.out.println(map.get("java"));
// 输出:95
System.out.println(map.size());
// 输出:2
}
}List.copyOf:创建不可变列表副本
List.copyOf 复制列表当前结构,源列表之后的结构变化不会反映到结果。
// 语义:List.copyOf 复制列表当前结构,源列表之后的结构变化不会反映到结果。
import java.util.ArrayList;
import java.util.List;
public class ListCopyOfDemo {
public static void main(String[] args) {
List<String> listSource = new ArrayList<>(List.of("a"));
List<String> list = List.copyOf(listSource);
listSource.add("b");
System.out.println(list);
// 输出:[a]
}
}Set.copyOf:创建不可变集合副本
Set.copyOf 复制集合当前结构并拒绝 null,元素对象本身仍是浅引用。
// 语义:Set.copyOf 复制集合当前结构并拒绝 null,元素对象本身仍是浅引用。
import java.util.HashSet;
import java.util.Set;
public class SetCopyOfDemo {
public static void main(String[] args) {
Set<String> source = new HashSet<>(Set.of("a"));
Set<String> snapshot = Set.copyOf(source);
source.add("b");
System.out.println(snapshot);
// 输出:[a]
}
}Map.copyOf:创建不可变映射副本
Map.copyOf 复制映射当前结构并拒绝 null,键和值对象本身仍是浅引用。
// 语义:Map.copyOf 复制映射当前结构并拒绝 null,键和值对象本身仍是浅引用。
import java.util.HashMap;
import java.util.Map;
public class MapCopyOfDemo {
public static void main(String[] args) {
Map<String, Integer> source = new HashMap<>(Map.of("a", 1));
Map<String, Integer> snapshot = Map.copyOf(source);
source.put("b", 2);
System.out.println(snapshot);
// 输出:{a=1}
}
}不常用但需要知道
Collections.sort:旧式排序入口
Collections.sort 仍兼容旧代码,现代代码通常直接使用 list.sort(comparator)。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsSortDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(3, 1, 2));
Collections.sort(list);
// 关键变化:Collections.sort(list);;按 list 重排 Collections,返回 void;集合顺序更新。
System.out.println(list);
// 输出:[1, 2, 3]
}
}Collections.binarySearch:有序列表二分查找
列表必须按相同排序规则排好;未找到时返回负插入点编码,使用前先判断结果是否小于 0。
import java.util.Collections;
import java.util.List;
public class CollectionsBinarySearchDemo {
public static void main(String[] args) {
List<Integer> sorted = List.of(1, 3, 5, 7);
System.out.println(Collections.binarySearch(sorted, 5));
// 输出:2
}
}Collections.min:查找最小元素
min 按自然顺序或指定比较器返回最小元素,空集合调用会抛 NoSuchElementException。
import java.util.Collections;
import java.util.List;
public class CollectionsMinDemo {
public static void main(String[] args) {
List<Integer> values = List.of(3, 1, 3);
System.out.println(Collections.min(values));
// 输出:1
}
}Collections.max:查找最大元素
max 按自然顺序或指定比较器返回最大元素,空集合调用会抛 NoSuchElementException。
import java.util.Collections;
import java.util.List;
public class CollectionsMaxDemo {
public static void main(String[] args) {
List<Integer> values = List.of(3, 1, 3);
System.out.println(Collections.max(values));
// 输出:3
}
}Collections.frequency:统计元素出现次数
frequency 按 equals 统计目标元素在集合中出现的次数。
import java.util.Collections;
import java.util.List;
public class CollectionsFrequencyDemo {
public static void main(String[] args) {
List<Integer> values = List.of(3, 1, 3);
System.out.println(Collections.frequency(values, 3));
// 输出:2
}
}Collections.unmodifiableXxx:动态只读包装
只读包装阻止通过 view 修改,但仍反映 source 的后续变化;需要稳定快照时使用 copyOf。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableCollectionDemo {
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(source);
source.add("b");
System.out.println(view);
// 输出:[a, b]
}
}Collections.synchronizedCollection:同步包装边界
遍历同步包装集合时仍要按文档在包装对象上加锁;多步业务操作不自动成为原子事务。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
public class SynchronizedCollectionDemo {
public static void main(String[] args) {
Collection<String> values =
Collections.synchronizedCollection(new ArrayList<>());
// 关键变化:Collections.synchronizedCollection(new ArrayList<>());;调用 synchronizedCollection,实参为 new ArrayList<>();返回对底层 Collection 访问加同步包装的 Collection。
values.add("java");
synchronized (values) {
System.out.println(values.size());
// 输出:1
}
}
}ConcurrentHashMap:并发键值更新
ConcurrentHashMap 适合并发键值更新,但单键原子操作不能替代跨键事务。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapBoundaryDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("java", 1, Integer::sum);
// 关键变化:counts.merge("java", 1, Integer::sum);;向 counts 合并 "java", 1, Integer::sum,返回合并后的值;映射内容更新。
System.out.println(counts.get("java"));
// 输出:1
}
}CopyOnWriteArrayList:读多写少的并发列表
CopyOnWriteArrayList 的写入会复制底层数组,适合读多写少的监听器列表。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
List<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("audit");
System.out.println(listeners);
// 输出:[audit]
}
}简单案例
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SortSummaryDemo {
public static void main(String[] args) {
List<Integer> values = new ArrayList<>(List.of(3, 1, 2));
values.sort(Comparator.naturalOrder());
// 关键变化:values.sort(Comparator.naturalOrder());;按 Comparator.naturalOrder() 重排 values,返回 void;集合顺序更新。
System.out.println(values);
// 输出:[1, 2, 3]
}
}排序要先确认是否允许修改原列表;不允许时复制后排序。
易混点
- list.sort 和 Collections.sort 都是原地排序,而不是返回新列表;排序不可变列表会失败。
- List.of 等工厂是不可变容器,unmodifiableList 只是动态只读视图。
- copyOf 是浅结构复制,集合元素、Map 的键和值对象仍共享引用。
- Comparator 比较结果必须符合传递性;比较器错误会让 TreeSet 去重和排序结果都不可信。
课后小问
为什么排序前常常要复制 List? 答案:sort 会原地修改列表,复制后排序可以保留调用方传入的原始顺序。 解析:如果调用方已经明确允许修改,直接 sort 更简单;API 不应偷偷改变外部拥有的列表。
unmodifiableList 和 List.copyOf 的主要区别是什么? 答案:前者是源集合的动态只读视图,后者是当前结构的不可修改副本。 解析:源集合后续 add 会反映到包装视图,但不会反映到 copyOf 结果;两者都不深复制元素。
本节小结
- Comparator 组合让主键、次键、升降序和 null 规则可读地表达。
- 原地排序与复制排序应按调用方所有权决定。
- of/copyOf 适合不可修改结果,unmodifiableXxx 适合只读视图。
- 同步包装和并发集合有明确边界,不自动提供跨多步业务事务。
快速回顾
- 能用 comparing、thenComparing、reversed 组合排序。
- 能区分原地排序、不可变副本和只读视图。
- 能判断 null、重复元素和比较器一致性风险。
- 能说出同步包装、CopyOnWriteArrayList、ConcurrentHashMap 的适用边界。
阿源的知识库