JDK7 以上 Comparator限制探究

起因

前些时候线上用户反馈新上线的一个功能出现问题,查询日志发现关键词

Comparison method violates its general contract

上网查了一下

发现有这样一个规则

在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性

自反性:当 两个相同的元素相比时,compare必须返回0,也就是compare(o1, o1) = 0;

反对称性:如果compare(o1,o2) = 1,则compare(o2, o1)必须返回符号相反的值也就是 -1;

传递性:如果 a>b, b>c, 则 a必然大于c。也就是compare(a,b)>0, compare(b,c)>0, 则compare(a,c)>0

再回头看了一下自己项目的代码

Collections.sort(deptPaths,new Comparator<String>(){
    @Override
    public int compare(String o1, String o2) {
        return o2.split("/").length > o1.split("/").length ?  -1 : 1;
    }
});

重点是比较的部分,我写了一个非严格的比较(假设大于0是交换,则当前逻辑大于才交换,小于或者等于不交换),这很明显违反了自反性。知道了问题,立马修改比较函数变成

o1.split("/").length - o2.split("/").length

测试了下没有问题,先解决了线上问题

疑问

问题是解决了,但是仍有几个疑问

1.为什么jdk7排序算法会有这个比较器的限制

2.在测试的时候也有测试相同长度的数据,排序并未报错,这个报错到底是什么操作导致的

探究

1.为什么jdk7排序算法会有这个比较器的限制?

再看一遍日志,很明显最终报错的位置是在TimSort这个类

而TimSort是jdk7以后java的内置排序算法

TimSort是一种稳定排序算法,简单的说是归并排序和插入排序的优化算法,他需要严格单调去保证排序稳定性

对TimSort算法具体流程后续会写文章阐述,此处先解释具体出错的原因,继续打断点调试,扒下源码,找到了最终报错的位置

这是一个归并的操作,进行了块长度的检测,报错的原因实际上需要归并的块长度为0,归并排序中插入的位置不对,无法继续归并

可以看到这个归并排序的限制条件,即第一个块的第一个元素必须大于第二个块的第一个元素,第一个块的最后一个元素必须大于最后一个块的所有元素

* Merges two adjacent runs in place, in a stable fashion.  The first
* element of the first run must be greater than the first element of the
* second run (a[base1] > a[base2]), and the last element of the first run
* (a[base1 + len1-1]) must be greater than all elements of the second run.

那为什么要做这个限制呢

再深挖一点,可以找到这样一个函数,在这个归并函数中用作找插入点,也就是timsort 算法中的gallop模式

这是获取插入位置的函数,简单解释下gallop操作,这是timsort算法的一个核心优化点,在gallop操作时会设置一个阈值,用以判断代排列的元素是否基本有序

举个列子在[1,3,5,7,9,11,13,17,33,67]这样一个有序数组中插入37,从左到右需要比较10次,但是如果知道了其是有序的那么就可以用二分插入,插入次数就减少到了4次,而gallop操作就是在连续比较次数超过gallop阈值时且比较结果相同时采取二分的方式查找插入点。

问题也就出在这,为了简化,将gallop阈值设为3,如果是[1,1,1,1,1,1,1,2,2,2,4,4,5,6,6,7,11,88]这样的数组插入2,前三次1比较后,触发gallop操作,经过三次二分比较,插在第8个位置上,但是如果没有gallop操作常规插入,找到最近的大于点插入应该是第11位,很明显gallop的插入点就不满足稳定性了

虽然其实看起来排序结果没问题,实际上在报错出打断点,看a(slot_5)(需要排序的数组),也可以看到数据已经被排序完成,但是元素的顺序已经改变了,对于稳定排序的算法来说实际上就是结果错了

timsort算法需要保证稳定,从而保证“先到先得”,以确保更多使用场景的正确性,故而严格限制了比较函数,所以Java在实现时对归并排序做了限制,不符合归并条件的抛出异常,以确保排序稳定性

2.为什么有些不符合比较规则的排序没问题呢?

因为不会傻傻地一开始就去校验你的输入满不满足规则,只会在比较过程中才会去校验,如果你的 数据量小或者你的输入不是类这种有结构的输入,根本不会走timsort实际上会走插入排序和快排,即使走了timsort,能否触发gallop,触发gallop后插入位置是否不对,这些都满足了,才会在归并排序的过程中进行检查时报这个错。

补充

另外如果不想改比较函数或者比较函数比较难改

-Djava.util.Arrays.useLegacyMergeSort=true

设置jvm参数让其不走timsort也是可以的

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值