工作中的疑难问题-Comparison method violates its general contract!异常

8,410 阅读4分钟

刚开始看到这个问题,异常里提示at java.util.TimSort.mergeHi(TimSort.java:868)即TimSort类的mergeHi方法抛出的。
查看了Collections的sort方法源码,结果如下图

那为什么会出现这种异常呢?
因为JDK7中的Collections.Sort方法实现中,如果两个值是相等的,那么compare方法需要返回0,否则 可能 会在排序时抛错,而JDK6是没有这个限制的。
if (len2 == 0) {
throw new IllegalArgumentException("Comparison method violates its general contract!");
}
在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性,不然 Arrays.sort,

Collections.sort 会报 IllegalArgumentException 异常。

说明:

1) 自反性:x,y 的比较结果和 y,x 的比较结果相反。

2) 传递性:x>y,y>z,则 x>z。

3) 对称性:x=y,则 x,z 比较结果和 y,z 比较结果相同。(也叫可逆比较)

反例:下例中没有处理相等的情况,实际使用中可能会出现异常:
复现
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 > o2 ? 1 : -1;// 错误的方式
}
});
解决方案

先说如何解决,解决方式有两种。

修改代码方式1(官方的)
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
// return o1 > o2 ? 1 : -1;
return o1.compareTo(o2);// 正确的方式
}
});

修改代码方式1(自定义的)
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
if(o1 > o2) {
return 1;
} else if (o1< o2){
return -1;
} else {
//考虑==这种场景,同时注意o1 o2不为空
//假设a>b b>c 那么a>c一定成立的。
//目前上面的例子存在 问题是如果o1为null,则在任何情况下,都是null>o2,
//但是其实存在o2==null的情况,这就导致了null> null 的逻辑错误
return 0;
}
}
});
(想要真正解决这个问题,并不是像很多网上帖子上所说加个等于条件就可以,这个是因为逆比较引发的问题,就应该从根源上解决这个问题:就是根据自己的代码逻辑判断是否一定符合上述的自反性等特性,后面我会找出特殊的场景)

不修改代码

那么问题来了。为什么上面代码在JDK6中运行无问题,而在JDK7中却会抛异常呢?这是因为JDK7底层的排序算法换了,如果要继续使用JDK6的排序算法,可以在JVM的启动参数中加入如下参数:
-Djava.util.Arrays.useLegacyMergeSort=true
这样就会照旧使用JDK6的排序算法,在不能修改代码的情况下,解决这个兼容的问题。(这种本人测试未达到效果 可能是操作问题,我不建议使用这种 这种的弊端在我看来会导致你无法使用jdk1.7里面的新特性,对后期的升级是有不可知的影响的)
初步结论:
那么现在是否可以盖棺定论了,按照上面的分析来看,使用这种比较方式(return x > y ? 1 : -1;),只要集合或数组中有相同的元素,就会抛出本文标题的异常。实则不然,什么情况下抛出异常,还取决于JDK7底层排序算法的实现,也就是大名鼎鼎的TimSort。后面文章会分析TimSort。本文给出一个会引发该异常的Case,以便有心人共同研究,如下:
Integer[] array =
{0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 2, 30, 0, 3};

进一步探讨:
上述的东西是我查了一天的结论,但是依然没有解决我项目中实际的问题,如果添加了== 的场景还没有解决问题,基本上是compare里面重写的逻辑有误造成的,需要重新检查自己的代码是否有问题,但是这种一般隐藏的比较深,下面这个例子才跟我实际的情况比较相似

国外例子
下面代码,你可以看出为什么也会报这个错误吗?(这种场景隐藏的比较深,不反复思考根本发现不了)

外国人解释:
比较方法是不传递的。举个例子如果 A==B 和B==C,那么
A一定等于C。
现在看这个例子的情况:
假设A、B、C三个对象情况。假设包含情况是这样的:
childMap.containsKey(A.getID()) returns true
childMap.containsKey(B.getID()) returns false
childMap.containsKey(C.getID()) returns true
当A和B比较的时候,外面的if条件不满足,所以返回结果是0,意味着A==B
当B和C比较的时候,外面的if条件仍然不满足,所以结果也是0,意味着B==C。
假设A和C比较的时候很有可能返回1或-1 ,这就造成的结果是A!=C.
这就违反了传递规则。
所以不能在else里面直接返回0,需要根据情况判断。

至此我的问题才总算解决了.
参考博客:
blog.csdn.net/TomCosin/ar…
www.cnblogs.com/firstdream/…
www.jianshu.com/p/aebaa787c…