Comparison method violates its general contract报错解决

10,178 阅读3分钟

Ref

问题描述

public class FixedPromotionMaterialInfoComparator implements Comparator<PromotionMaterialInfo> {
    @Override
    public int compare(PromotionMaterialInfo o1, PromotionMaterialInfo o2) {
        try{
            if(StringUtils.isNotBlank(o1.getSortNum()) && StringUtils.isNotBlank(o2.getSortNum())){
                int sortNum1 = PromotionMaterialUtils.getSortNum(o1.getSortNum(), EasyUtils.toString(o1.getForceType()));
                int sortNum2 = PromotionMaterialUtils.getSortNum(o2.getSortNum(),EasyUtils.toString(o2.getForceType()));
                if(sortNum1 == sortNum2){ //页码相等 按照有效开始时间升序排序
                    Map<String, Object> extMap1 = JsonUtil.json2Map(EasyUtils.toString(o1.getExt()));
                    Map<String, Object> extMap2 = JsonUtil.json2Map(EasyUtils.toString(o2.getExt()));
                    Long startTimeVal1 = EasyUtils.parseLong(Optional.ofNullable(extMap1).orElseGet(HashMap::new).get("validStartTime"));
                    Long startTimeVal2 = EasyUtils.parseLong(Optional.ofNullable(extMap2).orElseGet(HashMap::new).get("validStartTime"));
                    return (int) (startTimeVal1 - startTimeVal2);
                }
                return sortNum1 - sortNum2; //页码不等 按照页码升序排序
            }
        }catch (Exception e){
            log.error("FixedPromotionMaterialInfoComparator exception,o1:{},o2:{}",JsonUtil.write2JsonStr(o1),JsonUtil.write2JsonStr(o2));
        }
        return 0;
    }
}

如上代码所示,线上运行后偶现如下报错日志。可以看到在调用 TimSort.sort 时, java.lang.IllegalArgumentException: Comparison method violates its general contract ,比较方法违背了其约定的规则。

java.lang.IllegalArgumentException: Comparison method violates its general contract!  
at java.util.TimSort.mergeHi(TimSort.java:868)  
at java.util.TimSort.mergeAt(TimSort.java:485)  
at java.util.TimSort.mergeCollapse(TimSort.java:408)  
at java.util.TimSort.sort(TimSort.java:214)  
at java.util.TimSort.sort(TimSort.java:173)  
at java.util.Arrays.sort(Arrays.java:659)  
at java.util.Collections.sort(Collections.java:217) 

问题分析

在 JDK6 及更老版本的中,上述代码运行是不会产生异常的。从 JDK7 开始,Collections.sort() 在排序算法上的更新固然能够带来排序性能上的提升,但这一次排序算法的升级对比较器 Comparator 增加了一些规则,并没有完全向前兼容。

在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性,不然 Arrays.sortCollections.sort 会报 IllegalArgumentException 异常。

  1. 自反性:x,y 的比较结果和 y,x 的比较结果相反,即 sgn(compare(x,y)) == -sgn(compare(y,x))
  2. 传递性:x > y,y > z,则 x > z,即 ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0
  3. 对称性:x=y,则 x,z 比较结果和 y,z 比较结果相同。即 compare(x,y)==0 implies that sgn(compare(x,z))==sgn(compare(y,z)) for all z

因此,对于上述代码,compare(PromotionMaterialInfo o1, PromotionMaterialInfo o2),若 o1.startTimeVal1 = o2.startTimeVal1,则会出现 compare(o1,o2) == compare(o2,o1) 的情况,这违反了自反性质。从而导致代码抛出异常。

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. 方法1:强制 JVM 使用老旧的 MergeSort,而非新的 TimSort。 (1) 可以在代码层面上进行声明
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

(2) 也可以在 JVM 的启动参数中声明

-Djava.util.Arrays.useLegacyMergeSort=true
  1. 方法2:修改代码,使得比较器 Comparator 满足新算法自反性、传递性、对称性的要求
public class FixedPromotionMaterialInfoComparator implements Comparator<PromotionMaterialInfo> {
    @Override
    public int compare(PromotionMaterialInfo o1, PromotionMaterialInfo o2) {
        try{
            if(StringUtils.isNotBlank(o1.getSortNum()) && StringUtils.isNotBlank(o2.getSortNum())){
                
                ... 

                //return (int) (startTimeVal1 - startTimeVal2);
                return startTimeVal1.compareTo(startTimeVal2);  //修改为此语句
            }
            
            ...
        }catch (Exception e){
            log.error("FixedPromotionMaterialInfoComparator exception,o1:{},o2:{}",JsonUtil.write2JsonStr(o1),JsonUtil.write2JsonStr(o2));
        }
        return 0;
    }
}

其中 compareTo() 源码如下。

    /**
     * Compares two {@code Long} objects numerically.
     *
     * @param   anotherLong   the {@code Long} to be compared.
     * @return  the value {@code 0} if this {@code Long} is
     *          equal to the argument {@code Long}; a value less than
     *          {@code 0} if this {@code Long} is numerically less
     *          than the argument {@code Long}; and a value greater
     *          than {@code 0} if this {@code Long} is numerically
     *           greater than the argument {@code Long} (signed
     *           comparison).
     * @since   1.2
     */
    public int compareTo(Long anotherLong) {
        return compare(this.value, anotherLong.value);
    }
    
    /**
     * Compares two {@code long} values numerically.
     * The value returned is identical to what would be returned by:
     * <pre>
     *    Long.valueOf(x).compareTo(Long.valueOf(y))
     * </pre>
     *
     * @param  x the first {@code long} to compare
     * @param  y the second {@code long} to compare
     * @return the value {@code 0} if {@code x == y};
     *         a value less than {@code 0} if {@code x < y}; and
     *         a value greater than {@code 0} if {@code x > y}
     * @since 1.7
     */
    public static int compare(long x, long y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }