【踩坑系列】Java的Comparator:Comparison method violates its general contract!

2,367 阅读2分钟

前言

在业务开发中,我们经常会遇到对一个集合对象里面的元素按一定规则排序的需求。在Java中,通常我们会调用集合对象对应的sort方法,传入一个Comparator的匿名内部类,来实现业务需求。

背景

恰巧最近我在业务中就碰到这么个需求,需要对包含Staff的List集合对象按照Staff的注册时间倒序返回结果。

Staff定义:

public class Staff{
    // 员工的注册时间
    private Date registerDate;
    .
    .
    .
}

主要业务代码:

...
List<Staff> staffs = staffRepository.findByUser(account.getUser());
        if(staffs != null && staffs.size() > 0) {
            // 主要代码,根据注册时间倒序返回staff记录
            staffs.sort((o1, o2) -> {
                        if (o1.getRegisterDate().after(o2.getRegisterDate())) {
                            return -1;
                        } else {
                            return 1;
                        }
                    }
            );
...

问题

当时写完,随便跑了下也没问题,就没放在心上。直到前两天(也就是清明节)的凌晨一点,一顿亡命电话call,把我从睡(mei)梦(shui)中叫醒,我才知道自己犯了一个多蠢的错误。

同事当时发过来一段线上日志:Comparison method violates its general contract!

这段日志的大致意思是,比较方法违反了它的自然原则。google一下,发现原来是自己错误使用Comparator导致的。

分析

翻下官方文档对compare方法的doc解释:


其中最重要的就是中间讲到的三点:

  1. sgn(compare(x,y)) == -sgn(compare(y,x))
  2. compare(x,y)>0 && compare(y,z)>0 ==> compare(x,z)>0
  3. compare(x,y)==0 ==> 对任意z,都有sgn(compare(x,z))==sgn(compare(y,z))

回想我们上面的业务代码实现,显然第一点都没有满足: 当o1, o2 的registerDate相同时,o1, o2 在参数中位置的不同会导致排序结果的不同,自相矛盾。

解决方案

把关键的业务代码替换为:

staffs.sort((o1, o2) -> - o1.getRegisterDate().compareTo(o2.getRegisterDate())

即可解决问题。对于相同注册时间的o1, o2 元素,会返回0(即二者在排序上相同),不会因为参数顺序出现自相矛盾的结果。

总结

在实现自定义的排序规则时,一定要考虑清楚,检查是否符合上面说所的三个原则。