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没有这样的值。

文/程忠 浏览次数:0次   2021-09-30 10:21:03

相关阅读


评论:
点击刷新

↓ 广告开始-头部带绿为生活 ↓
↑ 广告结束-尾部支持多点击 ↑