先说结论

Comparable 由类自身实现 compareTo,定义唯一的自然顺序;Comparator 是独立策略,可以为同一类型提供多种排序方式。业务类型不便修改或存在多种排序规则时优先使用 Comparator。

比较器返回负数、零、正数分别表示小于、等于、大于,不应该用两个整数直接相减,因为可能溢出。

正确写法

使用 Comparator.comparingthenComparingnullsFirst/nullsLast 组合规则,可读性和边界处理更好。比较关系应满足反对称、传递和一致性,否则排序算法和 TreeSet 可能表现异常。

一段完整比较器

Comparator<Employee> byDepartmentAndSalary =
    Comparator.comparing(Employee::department,
                         Comparator.nullsLast(String::compareTo))
              .thenComparing(Employee::salary, Comparator.reverseOrder())
              .thenComparingLong(Employee::id);

它先按部门升序且 null 放最后,再按工资降序,最后用唯一 ID 打破平局。最后的稳定唯一字段对分页、TreeSet 去重和可重复测试结果都很重要。

基本类型字段优先使用 comparingIntcomparingLongcomparingDouble,避免每次比较都装箱。昂贵的派生值不要在比较器中反复计算,可以先做 Schwartzian transform 式预计算或缓存排序键。

比较契约

一个正确比较器至少满足:

  • 反对称:sign(compare(a,b)) == -sign(compare(b,a))
  • 传递:a > b 且 b > c,则 a > c。
  • 相等一致:compare(a,b) 为 0 时,与 a 等价的比较结果应和 b 一致。
  • 最好与 equals 一致,若不一致要在文档中明确。

违反传递性不仅结果“顺序奇怪”,排序实现还可能抛出 Comparison method violates its general contract

为什么不能直接相减

// 错误:可能整数溢出
(a, b) -> a.getScore() - b.getScore()

// 正确
Comparator.comparingInt(Player::getScore)

当 a 是 Integer.MAX_VALUE、b 是负数时,相减可能溢出为负值,颠倒大小关系。浮点数还要考虑 NaN、正负零,使用 Double.compare 更安全。

稳定排序是什么意思

稳定排序会保持“比较结果为 0”的元素原始相对顺序。Java 对对象列表的排序提供稳定性,但 PriorityQueue 和 TreeSet 的语义不同。若业务要求确定性输出,不应只依赖输入恰好稳定,最好添加明确 tiebreaker。

自然顺序设计

只有类型存在公认且长期稳定的唯一自然顺序时才实现 Comparable,例如日期按时间。员工可能按姓名、工号、入职时间或绩效排序,没有唯一答案,更适合提供多个命名 Comparator。

集合影响

TreeSet 和 TreeMap 以比较结果为 0 判断键是否重复。比较器若只比较姓名,两个 id 不同但同名的对象可能只保留一个,因此比较字段必须符合目标集合的唯一性语义。

容易踩坑的地方

排序稳定性由算法和 API 契约决定,不是 Comparator 自己保证。比较器与 equals 不一致虽然语法允许,但容易让有序集合行为违背直觉。

常见问题

追问:如何按多个字段排序?

先确定主排序字段,再通过 thenComparing 添加次级字段,并明确每个字段的升降序与 null 位置。

追问:reversed 应该放在哪里?

comparing(...).reversed() 会反转截至当前的整个比较器;只想反转某个字段时,把该字段自己的 Comparator 设为 reverseOrder,再 thenComparing 其他字段。

追问:Comparator 可以序列化吗?

只有比较器对象及捕获内容可序列化时才可能安全序列化。协议中保存比较器通常不是好设计,建议保存稳定排序规则标识并在接收端重建。

追问:TreeMap 使用的比较器可以后改吗?

不能原地替换。树结构按创建时规则组织;需要新规则应创建新 TreeMap 并重新插入所有数据。