java.lang.IllegalArgumentException:Comparison method violates its general contract!
这个异常是一个很坑的异常,异常在调用Collections.sort()方法时产生。
具体异常信息如下:
Comparison method violates its general contract!
java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.TimSort.mergeHi(TimSort.java:899)
at java.util.TimSort.mergeAt(TimSort.java:516)
at java.util.TimSort.mergeCollapse(TimSort.java:439)
at java.util.TimSort.sort(TimSort.java:245)
at java.util.Arrays.sort(Arrays.java:1512)
at java.util.ArrayList.sort(ArrayList.java:1462)
at java.util.Collections.sort(Collections.java:175)
at com.dancen.serverdog.handler.backup.BackupFileSetManager.sortLocalFiles(BackupFileSetManager.java:591)
at com.dancen.serverdog.handler.backup.BackupFileSetManager.listLocalFiles(BackupFileSetManager.java:523)
at com.dancen.serverdog.handler.backup.BackupFileSetManager.getCopyFileSet(BackupFileSetManager.java:85)
at com.dancen.serverdog.handler.backup.BackupExecutor.execute(BackupExecutor.java:84)
at com.dancen.serverdog.domain.backup.BackupItem.execute(BackupItem.java:132)
at com.dancen.serverdog.handler.backup.BackupRunnable.run(BackupRunnable.java:94)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
产生问题的代码如下:
private List<File> sortLocalFiles(List<File> files){List<File> rs = null;if(null != files){Comparator<File> comparator = new Comparator<File>(){@Overridepublic int compare(File sourceFile, File targetFile){int rs = 0;if(null != sourceFile && null != targetFile){Long sourceFileLength = sourceFile.length();Long targetFileLength = targetFile.length();rs = sourceFileLength.compareTo(targetFileLength);}return rs;}};rs = new ArrayList<File>(files);Collections.sort(rs, comparator);}return rs;}
异常原因:
这段代码看似没有任何问题,为什么会产生异常呢?
Collections.sort()在JDK6和JDK7中实现的底层排序算法发生了变化,在JDK6中使用MergeSort排序,而在JDK7中使用TimSort排序。TimSort相比MergeSort具备了更好的性能,但同时也对比较器Comparator有了更高的要求:
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 那么sgn(compare(x, z))==sgn(compare(y, z))
即新的排序算法要求比较器Comparator满足:
1. 自反性
2. 传递性
3. 对称性
三个条件的约束。
简单看来,作为一个比较器来说,以上3个要求似乎合情合理,理所当然,但事实上,开发者很容易忽略一些特定的情况,因为以上3个要求对于排序来讲,并不是全部必要的。
正如以上的代码,看似没毛病,因为根据实现已经完全能够满足排序的需要了。但是在比较器的参数为null的情况下,该实现不能满足上面所说的对称性,且这一点容易被开发者忽略:
x=null; y=yFile; z=zFile
compare(x,y)==0; compare(x,z)==0;
但compare(y,z)不一定==0,不能保证sgn(compare(x, z))==sgn(compare(y, z))。
有几点需要说明:
1. 以上代码在JDK6及更老版本的JDK中运行是完全不会产生异常的。
2. 在代码层面上保证比较器Comparator的参数不为null不是一件复杂的事情,但这并不能避免TimSort抛出异常,这需要从TimSort的源码上深入分析,个人猜测或许在算法的某些优化上会出现自己引入null值的情况。
3. 该异常的产生具有偶然性,并不容易重现,这给调试和查找问题带来了困难。事实上,该异常需要满足特定的条件才会产生,如待排序List的容量达到32等。也正是如此,这个异常在开发阶段往往能够潜伏起来,到了复杂的线上环境后才时不时出现。
解决办法:
Collections.sort()在排序算法上的更新固然能够带来排序性能上的提升,但这一次排序算法的升级对比较器Comparator增加了一些规则,并没有完全向前兼容,更由于增加的规则是隐性的,这就使得开发人员在无意之间制造了线上环境“万万想不到”的异常,甚至造成线上环境的崩溃,产生损失。在一定的程度上甚至可以说,这一升级是得不偿失的。
解决方法之一:
强制JVM使用老旧的MergeSort,而非新的TimSort。
1. 可以在代码层面上进行声明:
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
2. 也可以在JVM的启动参数中声明:
-Djava.util.Arrays.useLegacyMergeSort=true
解决方法之二:
第二种解决办法自然是进行代码上的修改,使得比较器Comparator满足新算法自反性、传递性、对称性的要求。
修改后的代码如下:
private List<File> sortLocalFiles(List<File> files){List<File> rs = null;if(null != files){Comparator<File> comparator = new Comparator<File>(){@Overridepublic int compare(File sourceFile, File targetFile){Long sourceFileLength = null == sourceFile ? -1 : sourceFile.length();Long targetFileLength = null == targetFile ? -1 : targetFile.length();return sourceFileLength.compareTo(targetFileLength);}};rs = new ArrayList<File>(files);Collections.sort(rs, comparator);}return rs;}