关于使用Comparable接口产生java.lIllegalArgumentException:Comparison method violates its general contract异常浅析

1.Comparable接口


Comparable接口是一个高频接口,Java中用它对类进行排序,排序的重要性不言而喻。

接口中只有一个方法

public int compareTo(T o);

看上去使用方法很简单,把要排序的逻辑写好即可。但背后涉及了很多细节问题,其中之一就有Java规范的问题,不了解这些规范,很有可能编写出有问题的排序逻辑。

2. 业务场景分析及解决


业务场景是要对产品进行排序,首先根据产品的状态排序,再根据收益率排序,假设状态分为A、B、C、D四种,其对应的整形值分别为1、0、5、(3、4),D状态对应两种整型值。

初始伪代码为:

 	@Override
    public int compareTo(Product product) {
    	//estYield为收益率,类型是String
        if (this.status.equals(product.getStatus()))
            return -this.estYield.compareTo(product.getEstYield());
        if (this.status.equals(1))
            return -1;
        if (this.status.equals(0)) {
            if (product.getStatus().equals(1))
                return 1;
            return -1;
        }
        if (this.status.equals(5)) {
            if (product.getStatus().equals(1) || product.getStatus().equals(0))
                return 1;
            return -1;
        }
        return 0;
    }

A(对应值为1)最大,其次是B(对应值为2),再其次是C(对应值为5),最后是D(对应值为3、4),每种状态下再根据收益率从大到小排序,看上去似乎没什么问题。确实,想法是没问题的,但是代码上存在逻辑漏洞。用的是Collections.sort()对集合进行排序。测试环境数据少,并没有检测出问题,生产环境数据量大,结果出了问题。

Collections.sort()在不同数据量情况下用了不同的算法,真的是策略模式的典型应用,让使用者完全无感知。重点是分析产生问题的原因。

假设:

  1. 有一个产品M的状态为3,收益率为3,另一个产品N状态也为3,收益率为3.3,那么根据上面的排序规则,状态都为3,相同,收益率3.3大于3,所以,产品N大于产品M
  2. 又有一个新产品L,状态为4,收益率为3.3,与N相比,根据排序规则,状态3和状态4相同(因为代码最后一行返回0意味着3和4是相同的),收益率也相同,所以,产品L等于产品N基于等号的传递性,意味着产品L等于产品M
  3. 但是,再单独比较产品L与产品M,状态4和3,意味着相同,接着比较收益率,3.3大于3,所以,产品L大于产品M
  4. 出现问题了,在2中得出L等于M,在3中得出L大于M,所以排序规则代码是存在问题的,原因在于D状态有两个对应的值对象3和4,状态一致时比较收益率,本身没什么问题,需要对D状态做更细致的划分,规定3比4大,或者4比3大,然后再根据收益率排序,问题就能迎刃而解,不会再存在违背等号传递性的问题

虽然这样意味着状态3比状态4大(或者状态4比状态3大),同属于D状态本应不区分大小,但不区分是会有问题存在的,为了解决问题,只能再细致的划分一下。

解决后的伪代码:

	@Override
	public int compareTo(Product product) {
       if (this.status.equals(product.getStatus()))
           return -this.estYield.compareTo(product.getEstYield());
       if (this.status.equals(1))
           return -1;
       if (this.status.equals(0)) {
           if (product.getStatus().equals(1))
               return 1;
           return -1;
       }
       if (this.status.equals(5)) {
           if (product.getStatus().equals(1) || product.getStatus().equals(0))
               return 1;
           return -1;
       }
       if (this.status.equals(3)) {
           if (product.getStatus().equals(4))
               return -1;
           return 1;
       }
       return 1;
   }

3.实现Comparable接口要遵循的通用约定


关于Java中的通用约定,在《Effective Java》第14条考虑实现Comparable接口中有着细致描述,如下:

  1. 实现者必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。(这也暗示着,当且仅当y.compareTo(x)抛出异常时,x.compareTo(y))才必须抛出异常。
  2. 实现者还必须确保这个关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z)>0)暗示着x.compareTo(z)>0。
  3. 最后,实现者必须确保x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z))==sgn(y.compareTo(z))。
  4. 强烈建议(x.compareTo(y) == 0) == x.eqauls(y)),但这非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个类,都应该明确予以说明。推荐使用这样的说法:“注意:该类具有内在的排序功能,但是与equals不一致”

可知,第3条中写着满足传递性,x == y以及x == z,要保证y == z。而最初的代码中,不知不觉间违背此通用约定,以至于产生了

java.lang.IllegalArgumentException: Comparison method violates its general contract!

这个异常。

相信还会有很多细节问题存在于代码的方方面面,要审慎的思考,再编写代码。

End

你可能感兴趣的:(工作问题,java,后端)