JAVA 排序异常:java.lang.IllegalArgumentException:Comparison method violates its general contract!

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;
	}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值