业务排序代码的几个"坏味道"
代码 Review 时经常看到这样的排序:
java
// 写法一:手写 Comparator,逻辑臃肿
list.sort((a, b) -> {
if (a.getScore() != b.getScore()) {
return b.getScore() - a.getScore();
}
return a.getCreateTime().compareTo(b.getCreateTime());
});
// 写法二:整数相减的"陷阱写法"
list.sort((a, b) -> b.getScore() - a.getScore());
这些写法或者有 bug,或者可读性差,或者在多字段排序时难以维护。翻开 JDK 8 的 Comparator 源码,里面早就准备好了更好的工具。
一、Comparator 的函数式 API:源码解析
comparing:从提取 key 到比较
java
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
本质是:先用 keyExtractor 从对象中提取一个可比较的 key,再用 key 的自然顺序比较。
对业务代码的影响:这意味着你只需要告诉 Comparator "按哪个字段排",不需要写比较逻辑:
java
// ✗ 传统写法:你在写"怎么比"
list.sort((a, b) -> a.getScore().compareTo(b.getScore()));
// ✓ 函数式写法:你在说"按什么排"
list.sort(Comparator.comparing(UserVO::getScore));
thenComparing:多字段排序的链式写法
java
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
源码逻辑很清晰:第一个比较器结果不为 0 就用它,为 0 就用下一个。
对业务代码的影响:多字段排序从 if-else 嵌套变成了声明式的链式调用,可读性提升一个量级:
java
// ✗ 三字段排序的传统写法:难读、难维护、容易出错
list.sort((a, b) -> {
int r1 = b.getScore() - a.getScore();
if (r1 != 0) return r1;
int r2 = a.getLevel() - b.getLevel();
if (r2 != 0) return r2;
return a.getCreateTime().compareTo(b.getCreateTime());
});
// ✓ 链式写法:一眼看出排序规则
list.sort(
Comparator.comparingInt(UserVO::getScore).reversed()
.thenComparingInt(UserVO::getLevel)
.thenComparing(UserVO::getCreateTime)
);
comparingInt / comparingLong / comparingDouble:避免装箱
java
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1),
keyExtractor.applyAsInt(c2));
}
注意内部用的是 Integer.compare,不是减法。
对业务代码的影响:处理 int/long 字段时,用专属方法避免自动装箱。在大列表排序中,装箱产生的临时对象会增加 GC 压力:
java
// ✗ comparing 会触发 Integer 装箱
list.sort(Comparator.comparing(ProductVO::getStock));
// ✓ comparingInt 直接用基本类型,无装箱
list.sort(Comparator.comparingInt(ProductVO::getStock));
10 万条数据排序,comparing 会产生约 20 万次装箱(每次比较两个元素各装箱一次),comparingInt 是零装箱。
二、整数相减的陷阱:一个长期存在的 Bug
java
list.sort((a, b) -> b.getScore() - a.getScore());
这是一个整数溢出 bug 。当 b.getScore() = Integer.MAX_VALUE,a.getScore() = -1 时:
java
2147483647 - (-1) = 2147483648 > Integer.MAX_VALUE → 溢出为 -2147483648
溢出后比较结果反转,排序就乱了。
对业务代码的影响:这个 bug 在正常数据下不会触发,但在边界数据(如金额、积分、时间戳差值)下可能出现。而且一旦出现,排序结果错误但不会抛异常,非常难排查。
java
// ✗ 有溢出风险
list.sort((a, b) -> b.getScore() - a.getScore());
// ✓ 方式一:Integer.compare,永远不溢出
list.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
// ✓ 方式二:Comparator API(推荐)
list.sort(Comparator.comparingInt(UserVO::getScore).reversed());
Integer.compare 的源码很简单,但绝对安全:
java
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
用比较而非减法,从根本上避免了溢出。
三、TimSort:为什么业务不需要手动优化排序算法
Collections.sort 和 List.sort 用的是 TimSort
java
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
TimSort 融合了归并排序和插入排序的优点:
-
对近乎有序的数据(业务中很常见),接近 O(n)
-
最坏情况 O(n log n)
-
稳定排序:相等元素保持原始顺序
稳定排序在业务中的价值
业务场景:商品列表排序
需求:按销量降序,销量相同时保持原来的上架顺序(即数据库查出来的顺序)。
java
// 因为 TimSort 是稳定的,只需要按销量排一次
// 销量相同的商品会保持查询时的原始顺序
productList.sort(Comparator.comparingInt(ProductVO::getSales).reversed());
如果排序算法不稳定,销量相同的商品顺序会被打乱,每次刷新页面商品位置都不一样,用户体验很差。
注意一点:你依赖"稳定排序保持原顺序"的前提,是你使用的排序实现确实是稳定的(这里讲的是 List.sort/Collections.sort 常见场景)。如果你换了排序 API 或实现方式,稳定性语义就要重新确认。
业务场景:多条件排序的拆分写法
利用稳定性,复杂的多条件排序可以拆成多次单条件排序(后排的优先级更高):
java
// 需求:先按部门排序,部门相同按职级降序,职级相同按入职时间升序
// 拆分写法(从最低优先级开始排):
list.sort(Comparator.comparing(Employee::getHireDate)); // 第三优先级
list.sort(Comparator.comparingInt(Employee::getLevel).reversed()); // 第二优先级
list.sort(Comparator.comparing(Employee::getDeptName)); // 第一优先级
// 当然,链式写法更直观:
list.sort(
Comparator.comparing(Employee::getDeptName)
.thenComparingInt(Employee::getLevel).reversed()
.thenComparing(Employee::getHireDate)
);
拆分写法在某些场景下更灵活------比如排序条件是动态的,可以根据用户选择的排序字段逐步叠加。
四、处理 null:nullsFirst 和 nullsLast
业务数据里 null 很常见(用户没填的字段、外部接口返回的缺失数据),直接排序会 NPE。
java
// 源码
public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(false, comparator);
}
NullComparator 的 compare 方法会先判断 null,再用内部 comparator 比较非 null 元素。
业务场景:用户列表按最后登录时间排序,未登录过的排最后
java
// ✗ 手写 null 处理,又臭又长
list.sort((a, b) -> {
if (a.getLastLoginTime() == null && b.getLastLoginTime() == null) return 0;
if (a.getLastLoginTime() == null) return 1;
if (b.getLastLoginTime() == null) return -1;
return b.getLastLoginTime().compareTo(a.getLastLoginTime());
});
// ✓ 一行搞定
list.sort(
Comparator.comparing(
UserVO::getLastLoginTime,
Comparator.nullsLast(Comparator.reverseOrder())
)
);
业务场景:导出报表,部门和姓名都可能为 null
java
Comparator<EmployeeVO> comparator = Comparator
.comparing(EmployeeVO::getDeptName,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(EmployeeVO::getName,
Comparator.nullsLast(Comparator.naturalOrder()));
employeeList.sort(comparator);
注意 comparing 的双参数重载:第二个参数是用于比较 key 的 Comparator,可以在这里嵌入 null 处理。
五、Comparable vs Comparator:业务中该实现哪个
Comparable:对象的自然顺序
java
public class Order implements Comparable<Order> {
@Override
public int compareTo(Order other) {
return this.createTime.compareTo(other.createTime);
}
}
适合对象有且只有一种"天然"排序方式。
Comparator:外部定义的排序规则
适合排序规则多样、在不同场景使用不同规则的情况。
对业务代码的影响:
-
VO 类(展示层):通常不实现 Comparable,因为不同页面的排序规则不同。用 Comparator 在需要时排序。
-
领域模型(有明确自然序):可以实现 Comparable,比如
Version类按版本号排序。 -
不要在 Comparable 里写反自然序(如"按 score 降序"),这会让用
Collections.sort(list)的人困惑。
java
// ✗ 反直觉的 Comparable 实现
public class Product implements Comparable<Product> {
@Override
public int compareTo(Product other) {
return other.score - this.score; // 降序?调用者不看源码根本不知道
}
}
// ✓ 自然序应该是升序,降序交给调用方
public class Product implements Comparable<Product> {
@Override
public int compareTo(Product other) {
return Integer.compare(this.score, other.score); // 升序,符合直觉
}
}
// 调用方需要降序时
list.sort(Comparator.reverseOrder());
六、实战场景汇总
场景一:电商商品列表排序
需求:
1. VIP 商品排最前
2. 同等级内按销量降序
3. 销量相同按价格升序
4. 价格可能为 null(未定价商品),排最后
java
Comparator<ProductVO> comparator = Comparator
.comparing(ProductVO::isVip, Comparator.reverseOrder())
.thenComparingInt(ProductVO::getSales).reversed()
.thenComparing(
ProductVO::getPrice,
Comparator.nullsLast(Comparator.naturalOrder())
);
productList.sort(comparator);
场景二:后台管理列表的动态排序
前端传来排序字段和方向,后端动态构建 Comparator:
java
public Comparator<OrderVO> buildComparator(String sortField, String sortOrder) {
Comparator<OrderVO> comparator;
switch (sortField) {
case "amount":
comparator = Comparator.comparing(
OrderVO::getAmount, Comparator.nullsLast(Comparator.naturalOrder())
);
break;
case "createTime":
comparator = Comparator.comparing(
OrderVO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())
);
break;
default:
comparator = Comparator.comparingLong(OrderVO::getId);
}
if ("desc".equalsIgnoreCase(sortOrder)) {
return comparator.reversed();
}
return comparator;
}
// 使用
orderList.sort(buildComparator(request.getSortField(), request.getSortOrder()));
这种写法比在 SQL 里拼 ORDER BY 更安全(避免 SQL 注入),适合内存中排序的场景(如聚合多数据源后排序)。
场景三:排行榜(Top N)
如果只需要前 N 名,不需要全量排序。但 Comparator 的构建方式是一样的:
java
// 取销量前 10 的商品
List<ProductVO> top10 = productList.stream()
.sorted(Comparator.comparingInt(ProductVO::getSales).reversed())
.limit(10)
.collect(Collectors.toList());
注意:Stream.sorted() 底层也是 TimSort,limit(10) 不会提前终止排序(整个列表还是会被排完)。如果列表很大且只需要 Top N,考虑用 PriorityQueue(小顶堆):
java
// 大列表取 Top 10,用堆更高效:O(n log k) vs O(n log n)
PriorityQueue<ProductVO> heap = new PriorityQueue<>(
Comparator.comparingInt(ProductVO::getSales) // 小顶堆
);
for (ProductVO product : productList) {
heap.offer(product);
if (heap.size() > 10) heap.poll(); // 保持堆大小为 10
}
List<ProductVO> top10 = new ArrayList<>(heap);
top10.sort(Comparator.comparingInt(ProductVO::getSales).reversed()); // 堆取出后再排序
场景四:Excel 导出前的多级排序
java
// 导出员工花名册:按部门 → 职级降序 → 姓名拼音
Comparator<EmployeeExportVO> exportComparator = Comparator
.comparing(EmployeeExportVO::getDeptName,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparingInt(EmployeeExportVO::getLevel).reversed()
.thenComparing(EmployeeExportVO::getNamePinyin,
Comparator.nullsLast(Comparator.naturalOrder()));
employeeList.sort(exportComparator);
场景五:把 Comparator 抽成常量,团队复用
java
public class ProductComparators {
/** 默认排序:VIP优先 → 销量降序 → 上架时间升序 */
public static final Comparator<ProductVO> DEFAULT =
Comparator.comparing(ProductVO::isVip, Comparator.reverseOrder())
.thenComparingInt(ProductVO::getSales).reversed()
.thenComparing(ProductVO::getOnlineTime);
/** 价格排序:价格升序,null 排最后 */
public static final Comparator<ProductVO> BY_PRICE_ASC =
Comparator.comparing(ProductVO::getPrice,
Comparator.nullsLast(Comparator.naturalOrder()));
/** 最新上架 */
public static final Comparator<ProductVO> BY_NEWEST =
Comparator.comparing(ProductVO::getOnlineTime).reversed();
}
// 使用
productList.sort(ProductComparators.DEFAULT);
排序规则集中管理,避免散落在各个 Service 里重复编写。
七、Code Review 检查清单
java
// 🔍 看到整数相减做比较,立即标记
(a, b) -> b.getScore() - a.getScore() // 溢出风险
// 🔍 看到手写 null 判断的 Comparator,建议用 nullsFirst/nullsLast
if (a == null) return 1; // 用 Comparator.nullsLast 替代
// 🔍 看到多层 if-else 的排序逻辑,建议用链式 API
if (r1 != 0) return r1; ... // 用 thenComparing 替代
// 🔍 看到 comparing 用于 int/long 字段,建议用 comparingInt/Long
Comparator.comparing(VO::getIntField) // 有装箱开销,用 comparingInt
// 🔍 看到 Comparable 实现了降序,标记为反直觉
public int compareTo(X o) { return o.x - this.x; } // 不要在 Comparable 里写降序
总结:看了源码后,对业务排序逻辑的五个直接影响
-
丢掉整数相减 。
b.score - a.score有溢出 bug,用Integer.compare或comparingInt。这不是理论问题,金额、时间戳等大数值场景真的会触发。 -
多字段排序用链式 API 。
comparing().thenComparing()比 if-else 可读性高一个量级,新增排序字段只需要加一行.thenComparing()。 -
null 用 nullsFirst/nullsLast 处理。业务数据里 null 很常见,手写 null 判断容易遗漏边界。
-
基本类型用 comparingInt/Long/Double。大列表排序时避免装箱产生的 GC 压力。
-
信任 TimSort 的稳定性。利用稳定排序的特性,可以拆分多条件排序,也可以保证"相同优先级保持原始顺序"的业务需求。