看了 Comparator 源码,我重写了所有业务排序逻辑

业务排序代码的几个"坏味道"

代码 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_VALUEa.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 里写降序

总结:看了源码后,对业务排序逻辑的五个直接影响

  1. 丢掉整数相减b.score - a.score 有溢出 bug,用 Integer.comparecomparingInt。这不是理论问题,金额、时间戳等大数值场景真的会触发。

  2. 多字段排序用链式 APIcomparing().thenComparing() 比 if-else 可读性高一个量级,新增排序字段只需要加一行 .thenComparing()

  3. null 用 nullsFirst/nullsLast 处理。业务数据里 null 很常见,手写 null 判断容易遗漏边界。

  4. 基本类型用 comparingInt/Long/Double。大列表排序时避免装箱产生的 GC 压力。

  5. 信任 TimSort 的稳定性。利用稳定排序的特性,可以拆分多条件排序,也可以保证"相同优先级保持原始顺序"的业务需求。

相关推荐
likerhood3 小时前
Java final 关键字:从“不能改”到“安全发布”的深入理解
java·windows·安全
花千树-0104 小时前
SubAgent 基础:拥有自主工具的子代理
java·langchain·llm·agent·langgraph·subagent·harness
水上冰石4 小时前
java直接调用本地大模型文件,实现对话机器人
java·aigc·jlama
笨蛋不要掉眼泪4 小时前
Java并发编程:深入理解ThreadLocal
java·开发语言·jvm·并发
番茄去哪了4 小时前
JVM虚拟机(中)
java·开发语言·jvm
SimonKing4 小时前
从惊艳到踩坑:AI结对编程的真实复盘
java·后端·程序员
海兰4 小时前
【第56篇】Graph Example —— MCP-Node 模块
java·人工智能·spring boot·spring ai
程序猿乐锅4 小时前
【Tilas|第十篇】万字讲解SpringAOP知识点
java·开发语言·idea·tlias