Comparator.compare()必须返回负数、0或正数,严禁返回boolean/null/随机值;需防溢出、null异常、状态混用、链式短路误用、可变状态依赖。

Comparator.compare() 方法必须严格遵守三值契约
返回负数、0 或正数,分别代表“小于”“等于”“大于”。任何返回 boolean、null 或随机值的实现都会导致 Collections.sort() 抛出 IllegalArgumentException: Comparison method violates its general contract!。
常见错误包括:
- 用
a - b计算整数差(可能溢出,如Integer.MAX_VALUE - (-1)得负数) - 在比较字符串时直接用
==判空,未处理null值导致NullPointerException - 混用自然顺序和业务逻辑(比如先按状态排序再按时间,但状态枚举顺序与业务含义不一致)
lambda 表达式写 Comparator 要小心 null 安全
用 Comparator.comparing(Person::getName) 默认不允许 null;一旦列表中存在 name == null 的对象,运行时抛 NullPointerException。
安全写法需显式指定空值策略:
立即学习“Java免费学习笔记(深入)”;
Comparator.comparing(Person::getName, Comparator.nullsLast(String::compareTo))Comparator.comparing(Person::getAge, Comparator.nullsFirst(Integer::compareTo))- 若字段是基本类型包装类(如
Integer),注意Integer::compareTo本身可处理null,但前提是传入的Function不提前解包出null
链式比较器(thenComparing)的执行顺序不可逆
thenComparing 是“主序相等时才启用次序”,不是加权合并。比如:
Comparatorcmp = Comparator.comparing(Person::getStatus) .thenComparing(Person::getScore) .thenComparing(Person::getName);
这意味着:状态不同 → 直接按状态排,后面两个字段完全不参与比较;状态相同 → 才比分数;分数也相同 → 才比姓名。
容易踩的坑:
- 误以为
thenComparing能组合多个字段做“加权总分”,实际它只是短路式级联 - 对同一字段多次调用
thenComparing(如先升序再降序)——后一次会覆盖前一次,不会叠加 - 在
thenComparing中传入可能抛异常的 lambda(如p -> p.getDetail().getId()),而没做空检查,导致主序还没走完就崩了
Comparator 不能依赖可变状态或外部时间
排序过程可能被多次调用、并发复用,甚至被 JDK 内部缓存(如 TreeSet 构造时传入的 comparator)。如果 comparator 里读了当前时间、随机数、静态计数器或某对象的 mutable 字段,结果将不可预测、不可重现。
典型反例:
-
Comparator.comparing(p -> System.currentTimeMillis())—— 每次比较返回不同值,排序失败 -
new Comparator—— 状态污染,多线程下崩溃,单线程下结果依赖调用次数() { int count = 0; public int compare(...) { return count++; } } - 在
compare()里修改被比较对象的字段(如a.setSortFlag(true))——破坏函数式语义,后续逻辑错乱
真正稳定的自定义排序,只应基于输入参数的**只读属性**计算,且每次调用相同输入必得相同输出。










