Collections.sort的坑
jdk7以后Collections.sort这个方法变得很弱,一不小心就会踩坑,生产环境容易出事故。
首先看异常:
Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract! at java.util.TimSort.mergeLo(TimSort.java:777) at java.util.TimSort.mergeAt(TimSort.java:514) at java.util.TimSort.mergeCollapse(TimSort.java:441) 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 TestSort3.sort(TestSort3.java:34) at TestSort3.main(TestSort3.java:27)
关于这个错误,网上有些文章有说明,但基本都是说Comparator.compare方法未按规范返回三种值(-1,0,1)的。当然这是对的。
比如https://www.jianshu.com/p/4e568ef541ae
https://programtalk.com/java/comparison-method-violates-general-contract/
https://www.harinathk.com/java/sort-algorithm-changes-java-7-throws-illegalargumentexception/
本文说的是另一种情况,报错一样,原因不是Comparator.compare方法未按规范返回三种值的。这就是转入对象中的排序字段有NaN的。而且这也是一定概率才会发生,并非每次必现,以下的代码有我1/5左右的概率会复现。
import java.util.*; public class TestSort3 { public static void main(String[] args) throws Exception { //System.setProperty("java.util.Arrays.useLegacyMergeSort","true"); List<Product> prods=new ArrayList<>(); for(int i=0;i<400;i++){ //for(int i=0;i<200;i++){ Product rp=new Product(Double.valueOf(Math.random()).floatValue()); prods.add(rp); if(i%10==0){ rp.setScore(Float.NaN); } } sort(prods); /* //以下直接排序List<Float>的方式,多次试验并未报错,并且排序结果NaN的始终在最后(最大的) List<Float> prods=new ArrayList<>(); for(int i=0;i<400;i++){ Float val=Double.valueOf(Math.random()).floatValue(); if(i%10==0){ val=Float.NaN; } prods.add(val); } Collections.sort(prods); */ System.out.println(prods); } public static List<Product> sort(List<Product> items) { Collections.sort(items, new Comparator<Product>() { public int compare(Product a, Product b) { double acompete = (double)a.getScore(); double bcompete = (double)b.getScore(); if (bcompete - acompete > 0.0D) { return 1; } if (bcompete - acompete < 0.0D) { return -1; } return 0; } }); return items; } } class Product{ private float score; public Product(float score) { this.score = score; } public float getScore() { return score; } public void setScore(float score) { this.score = score; } @Override public String toString() { return "Product{" + "score=" + score + '}'; } }
其中的影响是否能复现的几个因素。
1.排序的必须是对象,而不能是Float
2.排序的集合数量要大,最好200以上
3.NaN的个数要合适,上面代码取的是%10的数
解决方法就简单了,
方法一:不使用TimSort.sort而使用LegacyMergeSort,在java命令加入
-Djava.util.Arrays.useLegacyMergeSort=true
或者在启动程序里加入:System.setProperty("java.util.Arrays.useLegacyMergeSort","true");
方法二:排序前检查数据,过滤掉NaN的排序字段的对象, 有这个用了方法一也不管用
有关的一个知识点:
Double val=Double.parseDouble("NaN"); Double val2=Double.valueOf("NaN");上面两行代码都不会报错,都会给Double一个正常的NaN值,Float也一样,Integer没有这样的值。
相关阅读
评论:
↓ 广告开始-头部带绿为生活 ↓
↑ 广告结束-尾部支持多点击 ↑