由于数值精度误差而违反compareTo传递契约的影响

时间:2017-10-30 15:18:24

标签: java guava compareto

我有一些我想要比较的数字。它们表示通过不同空间的路径长度。

对我来说不幸的是,一些不精确导致了错误的比较。例如,在注意到错误的效果之后,我发现我正在进行这样的比较:

a = 384.527100541296
b = 384.52710054129614 // Note the trailing 14 

就我的目的而言,a和b应该是平等的。

我注意到guava有一个fuzzyCompare()方法用于双打,似乎做了我想要的忽略一些精度:

private static final double COMPARISON_PRECISION=1e-10;

private static final Comparator<Double> fuzzyCompare= new Comparator<Double>(){
    public int compare(Double o1, Double o2) {
        return DoubleMath.fuzzyCompare(o1, o2, COMPARISON_PRECISION);
    }   
};

public int compareTo(Object o) {
    if (o instanceof Spam) {
       Spam other = (Spam) (o);
       return ComparisonChain.start()
       .compare(this.getLength(),other.getLength(),fuzzyCompare)
       //...
       .result();
    } else {
       throw new ClassCastException();
    }
}

关于模糊比较的警告并未引起我的注意:

  

这不是总排序,不适合用于   Comparable.compareTo(T)实现。特别是,事实并非如此   传递

我的问题是,这种缺乏传递性是一个真正的问题吗?如果是的话,它会如何呈现?我认为,如果比较真的被真正违反了,它会抛出类似于this question: Java error: Comparison method violates its general contract的错误,并且它甚至不会对我测试过的各种价值做出反应。

或者由于IllegalArgumentException是运行时错误,我还没有遇到问题,因为只有一些不正确的值会引发问题?

或许它现在做错了,它只是微妙到我没有注意到它?

3 个答案:

答案 0 :(得分:6)

TL; DR:

您的运营商不具有传递性。考虑a = 0b = 0.6c = 1.2,容差为1a==bb==c,但a!=c。解决方案是将值分区为类(例如通过舍入或截断),并使用Double.compare()来保持传递性。

详细说明:

首先让我们讨论一下使用fuzzyCompare(double, double, double)时您的数据是否具有传递性:

大多数情况下,您的数据将是可传递的,但是可以生成不是的数据。让我们采取以下值:

a = 384.52710054120
b = 384.52710054126
c = 384.52710054132

正如您所看到的,使用我们的新指标时,以下情况属实:a==bb==c,但a!=c。如您所见,您违反了transitivity

如果您的Comparator不具有传递性,是否会出现问题?

方法通过使用文档和/或注释来断言某些条件。 compare方法承诺该方法是可传递的。对许多案例来说,打破这种承诺可能是好的,但传递性并不重要,但依赖于这种承诺的代码可能会被打破。

如果传递的承诺被破坏,那么代码的示例可能无效?

让我们创建一个场景,其中我们有3个Foo类型的元素,这些元素根据一些名为Comparator的{​​{1}}不可传递。我们称他们为fooComparatorf1f2

f3

由于它们不是传递性的,我们假设Comparator<Foo> fooComparator = new Comparator<Foo>(){ public int compare(Foo o1, Foo o2) { // some non-transitive return value } }; &lt; f0f1&lt; f1f2&lt; f2成立。 如果你把它们放在一个列表中并尝试sort()它们会发生什么?

f0

如何解决问题

您可以通过将数据映射到另一个数据集来创建传递运算符,并使用在该集合上定义的传递运算符。让我们以较低的精度将实数映射到实数。

考虑以下值:

List<Foo> foos = new LinkedList<>();
Collections.addAll(f1, f2, f3)
Collections.sort(foos, fooComparator);

如果将它们截断为第二个数字(a = 0.01; b = 0.05; c = 0.13; d = 0.19; e = 0.21 )并使用Double.compare(),则会保留传递性。

您可以看到我们已将我们的值放入三个类Math.truncate(x * 10)/10。肯定有一些重要的定理证明了这种情况,但我无法记住它的名字......

答案 1 :(得分:1)

  

缺乏传递性是一个真正的问题

也许,取决于您尝试解决的问题。但是,如果代码期望Comparator的实现以可传递的方式工作,那么您可能遇到微妙的问题。除了&#34; undefined&#34;。

之外,很难说出效果是什么

如果我在评论中看到这段代码,我会感到非常高兴:您正在超载Java与您自己的比较明确的比较概念 - 有效但不同 - 比较的概念。

如果你称之为不同的东西 - fuzzyCompareFuzzyComparator等 - 这两种观念并不存在混淆。

答案 2 :(得分:1)

使用非传递性compareTo是一个可怕的想法:

  • 排序可能会抛出already pointed
  • 更糟糕的是,排序可能会返回错误的结果,可能完全错误。它依赖于你违反的合同,并且绝对不能保证它的结果很好(即有例外)。分析TimSort无济于事,因为算法可能会在几年内被更好的算法取代。
  • 任何SortedMap可能会非常突破。可能会发生您放置它的内容,将无法找到(对于HashMapequals已损坏的hashCode,这种情况确实会发生。同样,实施可能会在几年内发生变化,然后一切皆有可能。

我强烈建议以不同方式命名您的方法。或者,创建一个记录有相应警告的Comparator(这可能导致同样的问题,但它更加明确)。

请注意,如果遇到问题Comparable,即使HashMap可能会因许多冲突而中断,也会尽可能使用compareTo