Collections.sort()排序使用TimSort排序报Comparison method violates its general contract 原因

前段时间升级JDK后,之前的功能报java.lang.IllegalArgumentException: Comparison method violates its general contract 了。

去网上搜索了一下,前辈们的文章已经清楚的说明了问题的来源-----重写比较器的代码的时候,没有对相等的情况返回0. 

    @Override
    public int compareTo(OrgChartBean o) {
        if(o == null ||  o.getCount() < getCount()) {
            return 1;
        }
// 注释部分在代码中需要加上 否则用TimSort达到一定的条件就会报错
//		if(o.getCount() == getCount()) {
//			return 0;
//		}
        return -1;
    }

把上面注释代码提交后 不会出现报错问题 问题解决

现在闲下来想趁着出现的问题 学一学Collections.sort()中使用的TimSort排序算法 找找当时出现错误问题的原因。

为了找原因 先要得到一个会出现这个错误的数据。稍微看一下代码发现数据排序的List大于等于32 才会有多个run合并的操作 才有可能触发这个报错,所以写了一个生成随机数的程序生成32个50以内的随机数List 为了方便区分我把他相同的值只能有一对

得到数组

13, 14, 35, 11, 8, 33, 15, 29, 12, 0, 45, 20, 5, 37, 41, 9, 21, 7, 36, 6, 46, 22, 47, 32, 40, 5, 18, 27, 48, 16, 17, 23

测试程序如下

package cn.echohawk;
import java.io.Serializable;
import java.util.*;

public class TimSort {
    public static void main(String[] args) {
        List<OrgChartBean> resultList = new ArrayList<OrgChartBean>(32);
        long[] a = new long[]{13, 14, 35, 11, 8, 33, 15, 29, 12, 0, 45, 20, 5, 37, 41, 9, 21, 7, 36, 6, 46, 22, 47, 32, 40, 5, 18, 27, 48, 16, 17, 23};
        for(int i = 0 ; i < 32; i ++) {
            OrgChartBean bean = new OrgChartBean();
            bean.setCount(a[i]);
            resultList.add(bean);
        }
        String originalArr = resultList.toString();
        try {
            Collections.sort(resultList);
        } catch (Exception e) {
            System.out.println(resultList);
            System.out.println(originalArr);
            throw e;
        }
    }
}

class OrgChartBean implements Comparable<OrgChartBean>, Serializable {
    private static final long serialVersionUID = 1L;
    private long count;
    public OrgChartBean() {
        super();
    }
    public long getCount() {
        return count;
    }
    public void setCount(long count) {
        this.count = count;
    }
    @Override
    public int compareTo(OrgChartBean o) {
        if(o == null ||  o.getCount() < getCount()) {
            return 1;
        }
//		if(o.getCount() == getCount()) {
//			return 0;
//		}
        return -1;
    }

    @Override
    public String toString() {
        return "" + count;
    }
}

TimSort算法的详细代码分析可以查看Timsort详解  这篇博客已经对代码做了注释

这里就对大概的流程做一下说明

1.首先当排序的List大小小于32(MINMERGE)的时候,对整个List开始到结束单调递增或者单调递减的部分下标拿到,如果递增的则不变递减的把单调递减部分List反转 使其变为递增(java.util.ComparableTimSort#countRunAndMakeAscending) 然后以前段的单调递增List为基础 之后的元素一个个与前段基础List二分查找插入位置进行插入(java.util.ComparableTimSort#binarySort),最终结果得出。

2.1当List大小不小于32(MINMERGE)的时候,需要把List划分成多个部分排好序之后进行合并 当然最完美的状态是类似完整二叉树往上合并,所以划分的块数最好是完整二叉树的叶子节点的数量 即二的整数次幂(2,4,8...)  并且最好每个块要大小均匀而且维持在16到32之间的小块 这样效率最高 ,所以当前测试数据毫无疑问就分成两小块 每块16个元素(java.util.ComparableTimSort#minRunLength)

2.2接下来对每块的元素做单独的排序 排序方式依旧是1步骤所示 排序完成之后入栈 入栈之后当栈顶前三个元素 满足栈顶两元素的大小合并之后大于第三个的话 中间的元素和旁边两者中较小的元素合并  其他的情况就栈顶两元素合并(java.util.ComparableTimSort#mergeCollapse),当前测试用例就两块 所以排序好后 入栈就直接合并两个排好序的List 以下称呼两个数组为A1和A2

2.3合并操作首先会找到A1数组中A2[0]插入的话最大的下标位置如果相等的话就是最右边的下标 例如 [0,0,0,0,1] 中插入0的话 返回的就是4了 而不是0。上面测试用例导致报错问题的就是这个过程,当程序往右找的过程中当A1[1]和A2[0]比较的时候是相等的,但比较器并没有返回0 也就是说[0, 5, 8, 9, 11, 12, 13, 14, 15, 20, 29, 33, 35, 37, 41, 45, 这个数组中 5的话应该是返回2的 但实际返回的是1 (java.util.ComparableTimSort#gallopRight)

这样导致之后合并中间段数组的时候出现前后矛盾的现象,这里先把流程说完,再引出触发的问题。

之后再找到A2数组中A1[A1.length - 1]插入的话最大的下标位置如果相等的话就是最左边的下标 这里 [0,0,0,0,1] 中插入0的话 返回的就是0了 而不是4。这里A1[A1.length - 1]为45   A2为5, 6, 7, 16, 17, 18, 21, 22, 23, 27, 32, 36, 40, 46, 47, 48,  所以返回的是40所在的下标  所以最终两个数组就如下被分割成三份

[0,   5, 8, 9, 11, 12, 13, 14, 15, 20, 29, 33, 35, 37, 41, 45, 5, 6, 7, 16, 17, 18, 21, 22, 23, 27, 32, 36, 40,   46, 47, 48]

其中前部和尾部都不用进行合并了 因为前部肯定比中间部分的最小值要小 尾部肯定比中间部分的最大值要大

2.4中间部分进行合并 首先选出两个run中较小的一个run的长度 创建一个能放下这个数组的最小的2的整数次幂的临时数组 这边

A1 [0,   5, 8, 9, 11, 12, 13, 14, 15, 20, 29, 33, 35, 37, 41, 45]       所属中间部分15个

A2 [5, 6, 7, 16, 17, 18, 21, 22, 23, 27, 32, 36, 40,   46, 47, 48]     所属中间部分13个

所以把A2的数据做一个备份 temp 用一个大小为16的数组  因为备份的是A2 所以A2要先覆盖所以是找大的从A2往A1排

接下来就是每次从temp中取出最大值和A1中的最大值进行比较 谁大就把他放到A2的中部合并数组的末端 然后忽略这个值,继续比较较大的依次往前排  其中记录连续比较大的区块 如果区块连续七个都是大值的话 会启动Galloping Model   测试用例排序流程如下

 

到达了七次的时候 进入Galloping Model 会对下图中标红的剩余字段进行处理

2.5处理方式和操作2.3的手法一样 先以A1中的最大未分配元素5去temp插入的话最大的下标位置如果相等的话就是最右边的下标java.util.ComparableTimSort#gallopRight  即1 那么temp中的6, 7就已经是最大的了 复制到A1的第4,5个位置

接下来temp中的最小元素5去比较A1中的元素  比较之后发现A1中剩下的元素都比5小 因为这里temp的5 compareTo A1的5时候返回的是-1 说明temp的5 小于 A1的剩余元素  A1所有存留元素后移 这时候程序判断终止 因为A1没值的时候 temp肯定全部排完了 (因为2.3操作执行完成之后A1中间段的最小值必须大于temp中的所有值,但因为没有判断等于的情况,出现了temp中所有数据本来应该在A1中间段最小的数据之后的情况,没想到排完A1之后 temp中还有比最小值'小'的值 所以程序报错)

最后的文章的校正和结尾的更有说服力的描述改天梳理一下更新

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值