集合排序与比较器
排序是集合处理中非常常见的需求。Java 中排序主要依赖两套机制:
Comparable:对象自己定义自然顺序。Comparator:外部定义比较规则。
理解排序机制不仅能写出正确的排序代码,也能避免 TreeMap、TreeSet 中元素“莫名丢失”的问题。
# 1. Comparable
Comparable 表示对象自身可以比较。
public class User implements Comparable<User> {
private Long id;
private int age;
@Override
public int compareTo(User other) {
return Integer.compare(this.age, other.age);
}
}
排序:
List<User> users = new ArrayList<>();
Collections.sort(users);
自然顺序适合稳定、唯一、大家都认可的默认排序,例如:
- 数字按大小。
- 字符串按字典序。
- 日期按时间先后。
业务对象通常不一定有唯一自然顺序,因此更常用 Comparator。
# 2. Comparator
Comparator 是外部比较器。
users.sort(Comparator.comparing(User::getAge));
多字段排序:
users.sort(
Comparator.comparing(User::getAge)
.thenComparing(User::getId)
);
倒序:
users.sort(Comparator.comparing(User::getAge).reversed());
空值处理:
users.sort(
Comparator.comparing(
User::getName,
Comparator.nullsLast(String::compareTo)
)
);
# 3. compare 返回值
比较方法返回整数:
负数:this < other
零:this == other
正数:this > other
不要依赖具体返回值大小,只关心正负和零。
不推荐:
(a, b) -> a.getAge() - b.getAge()
风险:整数溢出。
推荐:
(a, b) -> Integer.compare(a.getAge(), b.getAge())
或者:
Comparator.comparingInt(User::getAge)
# 4. 比较器约定
比较器必须满足:
| 约定 | 说明 |
|---|---|
| 自反性 | compare(a, a) == 0 |
| 反对称 | compare(a, b) 与 compare(b, a) 符号相反 |
| 传递性 | a > b 且 b > c,则 a > c |
| 一致性 | 多次比较同一对象结果稳定 |
错误比较器可能导致排序异常、TreeMap 覆盖 key、TreeSet 丢元素。
# 5. List 排序
List<Integer> numbers = new ArrayList<>(List.of(3, 1, 2));
numbers.sort(Integer::compareTo);
或:
Collections.sort(numbers);
排序会直接修改原列表:
原列表 [3, 1, 2]
sort
原列表 [1, 2, 3]
如果不想修改原列表:
List<Integer> sorted = new ArrayList<>(numbers);
sorted.sort(Integer::compareTo);
# 6. Stream 排序
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getAge))
.toList();
stream().sorted() 返回新结果,不修改原集合。
对比:
| 方式 | 是否修改原集合 | 适合场景 |
|---|---|---|
list.sort | 是 | 原地排序 |
Collections.sort | 是 | 原地排序 |
stream.sorted | 否 | 生成排序后的新结果 |
# 7. TreeMap/TreeSet 排序
TreeMap 和 TreeSet 按比较器组织数据。
Set<User> set = new TreeSet<>(
Comparator.comparing(User::getAge)
);
如果两个用户 age 相同,比较结果为 0,TreeSet 会认为它们重复。
User{id=1, age=18}
User{id=2, age=18}
Comparator 只比较 age
compare 返回 0
TreeSet 只保留一个
修复:
Set<User> set = new TreeSet<>(
Comparator.comparing(User::getAge)
.thenComparing(User::getId)
);
比较器用于排序集合时,必须能区分业务上不应重复的对象。
# 8. null 排序
字段可能为 null 时:
users.sort(
Comparator.comparing(
User::getName,
Comparator.nullsLast(String::compareTo)
)
);
nullsFirst:
null, A, B, C
nullsLast:
A, B, C, null
不要在比较器里直接调用可能为 null 的字段方法。
# 9. 稳定排序
稳定排序指两个元素比较结果相等时,排序后仍保持原相对顺序。
原始:
A(age=18), B(age=20), C(age=18)
按 age 稳定排序:
A(age=18), C(age=18), B(age=20)
Java 对对象数组和 List 的排序通常是稳定排序。稳定性对多轮排序、报表排序很重要。
# 10. 排序性能
排序复杂度通常是 O(n log n)。比较器越复杂,排序成本越高。
优化建议:
- 避免比较器中调用远程接口或数据库。
- 避免比较器中做复杂计算。
- 可以先预计算排序字段。
- 大数据量排序尽量交给数据库或搜索引擎。
错误示例:
users.sort((a, b) -> {
BigDecimal scoreA = remoteService.queryScore(a.getId());
BigDecimal scoreB = remoteService.queryScore(b.getId());
return scoreA.compareTo(scoreB);
});
比较器会被调用很多次,这种写法会非常慢。
# 11. 常见业务排序
按创建时间倒序:
orders.sort(
Comparator.comparing(Order::getCreateTime).reversed()
);
按状态优先级:
Map<OrderStatus, Integer> priority = Map.of(
OrderStatus.PAID, 1,
OrderStatus.CREATED, 2,
OrderStatus.CANCELED, 3
);
orders.sort(Comparator.comparing(order -> priority.get(order.getStatus())));
按多个字段:
orders.sort(
Comparator.comparing(Order::getStatus)
.thenComparing(Order::getCreateTime, Comparator.reverseOrder())
.thenComparing(Order::getId)
);
# 12. 排序与数据库
如果数据来自数据库,优先考虑在 SQL 中排序:
ORDER BY create_time DESC, id ASC
适合 Java 内存排序的情况:
- 数据量不大。
- 排序字段由 Java 计算得出。
- 数据来自多个来源,需要合并后排序。
- 排序只是展示前的小范围处理。
大数据量分页排序不要拉到内存里做。
# Tips 快问快答
Q:Comparable 和 Comparator 有什么区别?
A:Comparable 是对象自身的自然顺序,Comparator 是外部传入的比较规则。
Q:比较器返回 0 表示什么? A:表示两个元素在当前比较规则下相等。
Q:为什么 TreeSet 会丢元素?
A:比较器返回 0 的元素会被认为重复,即使 equals 不相等。
Q:比较整数为什么不用相减?
A:可能溢出,应用 Integer.compare 或 Comparator.comparingInt。
Q:list.sort 会修改原列表吗?
A:会。
Q:stream.sorted 会修改原集合吗?
A:不会,它生成新的排序结果。
Q:字段可能为 null 怎么排序?
A:使用 Comparator.nullsFirst 或 nullsLast。
Q:排序一定要在 Java 内存里做吗? A:不是。数据库数据量大时优先用 SQL 排序。
Q:比较器里能调用远程接口吗? A:不建议,比较器会被调用很多次,性能和稳定性都差。
Q:多字段排序怎么写?
A:使用 Comparator.comparing(...).thenComparing(...)。