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>()
{
@Override
public 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不是一件复杂的事情,但谁能保证调用者的行为呢。
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>()
{
@Override
public 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;
}
BOOM!同样的报错又出现了!
已经修改了代码,为什么问题依然没有解决呢。仔细想想,应该只有一种可能了,在文件的排序过程中,文件的属性发生了变化,从而导致比较器出现冲突。例如,排序过程中,开始时文件X>文件Y,但是在排序完成之前,这两个文件被修改了,修改后文件X<文件Y,此时如果排序过程中再次对这两个文件进行对比,包括使用它们中的一个文件和其他文件进行对比,就会产生冲突。
再次吐槽:Collections.sort()在排序算法上的更新所产生的坑不小!
解决办法:
很难确保文件属性在排序过程中是静态的,要确保排序过程中不再出现异常,只有在排序之前先将待排序文件的属性缓存起来。
private List<File> sortLocalFiles(List<File> files)
{
List<File> rs = null;
if(null != files)
{
Map<String, Long> map = this.cacheFileLengthOfLocalFiles(files);
Comparator<File> comparator = new Comparator<File>()
{
@Override
public int compare(File sourceFile, File targetFile)
{
Long sourceFileLength = -1L;
Long targetFileLength = -1L;
if(null != sourceFile && map.containsKey(sourceFile.getPath()))
{
sourceFileLength = map.get(sourceFile.getPath());
}
if(null != targetFile && map.containsKey(targetFile.getPath()))
{
targetFileLength = map.get(targetFile.getPath());
}
return sourceFileLength.compareTo(targetFileLength);
}
};
rs = new ArrayList<File>(files);
Collections.sort(rs, comparator);
}
return rs;
}
private Map<String, Long> cacheFileLengthOfLocalFiles(List<File> files)
{
Map<String, Long> rs = null;
if(null != files)
{
rs = new HashMap<String, Long>();
for(File file : files)
{
if(null != file)
{
rs.put(file.getPath(), file.length());
}
}
}
return rs;
}