“比较方法违反其一般合同”仅在某些情况下被抛出

首先,我知道许多其他线程都描述了这个问题。 但是我无法找到并回答这个问题,为什么不总是抛出这个错误?

让我来描述一下我的意思。 我已经写了一些示例代码来说明这一点:

public class Mushroom { public int size; public Mushroom(int size) { this.size = size; } @Override public boolean equals(Object obj) { //this is intentionally false - read in description return false; } } 

DSA

 public class MushroomComparator implements Comparator { @Override public int compare(Mushroom o1, Mushroom o2) { // here is the code which breaks the contract if (o1.size o2.size){ return -1; } return 1; } } 

最后测试比较:

 public class ComparisonTest { public static void main(String[] args) { // System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"); List forest = new ArrayList(); for(int i =0; i<18; i++){ Mushroom mushroom1 = new Mushroom(1); Mushroom mushroom2 = new Mushroom(3); Mushroom mushroom3 = new Mushroom(2); forest.add(mushroom1); forest.add(mushroom2); forest.add(mushroom3); } Collections.sort(forest, new MushroomComparator()); } } 

在运行时,我们将收到此描述的问题

java.lang.IllegalArgumentException:比较方法违反了它的一般合同!

根据Collections.sort方法的文档:

(可选)如果实现检测到列表元素的自然顺序违反了Comparable合同

那么让我们回答一下这个问题在Comparable的文档中是什么(在我的例子中我使用Comparator,但是从文档中它应该满足相同的要求)

对于所有x和y,实现者必须确保sgn(x.compareTo(y))== -sgn(y.compareTo(x))

这个规则我故意打破以获得我正在写的错误。 这是compareTo方法的实现应该遵循的规则之一。 其余的都在文档中描述,但一般来说,据我所知,文档的等效关系应该得到满足

它紧接着来自compareTo契约,即商是C上的等价关系,并且自然顺序是C上的总顺序

现在真正困扰我的是我的测试方法中迭代次数变化的结果如果您将数字从18更改为22,那么将不会抛出exception。 此exception被描述为Optional,因此它确实意味着有时会抛出此exception,有时则不会。 我没有深入研究排序算法(TimSort)的新实现( 自Java 7以来改变排序算法 )。 我明白,为了打破比较器合同,检查每组数据可能会耗费一些CPU消耗,但是有时候会有什么意图表明,有时甚至没有。 它可能真的具有误导性。

Morover我可以将比较方法的实现改为简单..返回1.根据文档,它应该违反合同。 但事实并非如此。 在文档中也保留了与equals方法的合同,但它并不是真正需要的

强烈建议,但并非严格要求(x.compareTo(y)== 0)==(x.equals(y))

并且在我的示例中检查它我已经实现了equals方法以返回始终为false。 有了这个,我确信equals方法不会强制破坏比较器合同。

现在我的问题是:什么是比较合同(在打破它的背景下)? 为什么对于某些数据集会引发此exception,而对于其他数据则不会? 也许我错过了什么? 最后但并非最不重要的是 – 需要打破哪些规则以抛出此exception?

需要注意的是,解决这个问题的方法可能是关闭这个在这里描述的排序算法的新实现,并在我的示例代码中进行了评论。

为什么不总是抛出这个错误

因为Collections.sort不是validation比较方法的工作。 它的工作只是实现排序。 但这样做的逻辑可以揭示无效的比较方法作为其在某些条件分支中的逻辑的副产品 。 在这种情况下,抛出而不是尝试继续使用无效的比较方法进行排序是有意义的。

什么是比较器的真正合约(在打破它的背景下)?

如果你违反合同,排序可能无法正常运行,或者根本没有。 ( 并不是说它会validation你的比较方法。)

为什么对于某些数据集会引发此exception,而对于其他数据则不会?

如果排序没有遵循导致排序代码可以检测到的逻辑缺陷的路径,则它将不知道要抛出。 但是正在使用的TimSort 确实发生了一个逻辑分支,它揭示了无效的比较方法,所以它确实抛出。

如果 Collections.sort()有机会检测到你的比较器行为exception, 那么事实上总会抛出exception。 这种情况发生在某些罕见的情况下,其中sort()函数知道某个项目应该落在某个范围内,因为比较器先前已经指示过,现在它正在调用你的比较器并且它被告知该项目应该落在那个范围。 但是sort()非常复杂,它试图做尽可能少的工作,所以通常不会发生检测这种不当行为的条件。

如果sort()总是抛出exception,那么在开始对列表进行排序之前,它必须先发制人地调用你的比较器N ^ 2(即N平方)次数,以确保它总是尊重它的所有合同。列表中条目对的排列。 这将是非常低效的。

比较人必须遵守的合同在文件中列出:( https://docs.oracle.com/javase/7/docs/api/java/util/Comparator.html

  • 实现者必须确保所有x和y的sgn(compare(x, y)) == -sgn(compare(y, x))

  • 实现者还必须确保关系是传递的: ((compare(x, y)>0) && (compare(y, z)>0))表示compare(x, z)>0

  • 最后,实现者必须确保compare(x, y)==0意味着所有z的sgn(compare(x, z))==sgn(compare(y, z))

(其中sgn(x)是’Math.signum()’函数。)