在Java中,如何测试“Double”列表是否包含特定值

时间:2014-05-17 00:20:29

标签: java floating-point

背景:浮点数有舍入问题,因此不应将它们与" =="进行比较。

问题:在Java中,如何测试Double列表是否包含特定值。 我知道各种解决方法,但我正在寻找最优雅的解决方案,可能是那些利用Java或第三方库功能的解决方案。

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // should be 1.38, but end up with 1.3800000000000001
        Double d1 = new Double(1.37 + 0.01);
        System.out.println("d1=" + d1);

        // won't be able to test the element for containment
        List<Double> list = new ArrayList<Double>();
        list.add(d1);
        System.out.println(list.contains(1.38));
    }
}

输出是:

d1=1.3800000000000001
false

谢谢。

4 个答案:

答案 0 :(得分:4)

一般的解决方案是编写一个循环遍历列表的实用程序方法,并检查每个元素是否在目标值的某个阈值内。不过,我们可以使用Stream#anyMatch()

在Java 8中做得更好
list.stream().anyMatch(d -> (Math.abs(d/d1 - 1) < threshold))

请注意,我使用了建议here的等式测试。

如果你没有使用Java 8,我会写一个简单的实用方法:

public static boolean contains(Collection<Double> collection, double key) {
    for (double d : collection) {
        if (Math.abs(d/key - 1) < threshold)
            return true;
    }
    return false;
}

注意您可能需要为这两种方法添加一个特殊情况,以检查列表是否包含0(或使用abs(x - y) < eps方法)。这只需要在平等条件的末尾添加|| (abs(x) < eps && abs(y) < eps)

答案 1 :(得分:3)

比较比特并不是一个好主意。与另一篇文章类似,但涉及NaN和Infinities。

import java.util.ArrayList;
import java.util.List;

public class Test {


    public static void main(String[] args) {
        // should be 1.38, but end up with 1.3800000000000001
        Double d1 = 1.37d + 0.01d;
        System.out.println("d1=" + d1);

        // won't be able to test the element for containment
        List<Double> list = new ArrayList<>();
        list.add(d1);

        System.out.println(list.contains(1.38));
        System.out.println(contains(list, 1.38d, 0.00000001d));
    }

    public static boolean contains(List<Double> list, double value, double precision) {
        for (int i = 0, sz = list.size(); i < sz; i++) {
            double d = list.get(i);
            if (d == value || Math.abs(d - value) < precision) {
                return true;
            }
        }
        return false;
    }
}

答案 2 :(得分:1)

你可以将Double包装在另一个提供足够接近&#39;的课程中。等于方法的方面。

package com.michaelt.so.doub;

import java.util.HashSet;
import java.util.Set;

public class CloseEnough {
    private Double d;
    protected long masked;
    protected Set<Long> similar;

    public CloseEnough(Double d) {
        this.d = d;
        long bits = Double.doubleToLongBits(d);
        similar = new HashSet<Long>();
        masked = bits & 0xFFFFFFFFFFFFFFF8L; // 111...1000
        similar.add(bits);
        similar.add(bits + 1);
        similar.add(bits - 1);
    }

    Double getD() {
        return d;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof CloseEnough)) {
            return false;
        }

        CloseEnough that = (CloseEnough) o;

        for(Long bits : this.similar) {
            if(that.similar.contains(bits)) { return true; }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (int) (masked ^ (masked >>> 32));
    }
}

然后是一些代码来演示它:

package com.michaelt.so.doub;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<CloseEnough> foo = new ArrayList<CloseEnough>();
        foo.add(new CloseEnough(1.38));
        foo.add(new CloseEnough(0.02));
        foo.add(new CloseEnough(1.40));
        foo.add(new CloseEnough(0.20));
        System.out.println(foo.contains(new CloseEnough(0.0)));
        System.out.println(foo.contains(new CloseEnough(1.37 + 0.01)));
        System.out.println(foo.contains(new CloseEnough(0.01 + 0.01)));
        System.out.println(foo.contains(new CloseEnough(1.39 + 0.01)));
        System.out.println(foo.contains(new CloseEnough(0.19 + 0.01)));
    }
}

此代码的输出为:

false
true
true
true
true

(第一个假是与0比较,只是为了表明它没有找到那些不存在的东西)

CloseEnough只是一个围绕double的简单包装器,它掩盖了哈希码的最低三位(足够并且还存储了一组中相似数字的有效集合。当进行等于比较时,它使用集合。如果数字在集合中包含共同元素,则它们是相等的。

那就是说,我相当肯定有一些值会出现问题,a.equals(b)为真,而a.hashCode() == b.hashCode()为假,在边缘条件下仍可能出现正确的位模式 - 这样会做一些事情(比如HashSet和HashMap)&#39;不开心&#39; (并且可能会在某个地方提出一个好问题。


可能更好的方法是扩展ArrayList,以便indexOf方法处理数字之间的相似性:

package com.michaelt.so.doub;

import java.util.ArrayList;

public class SimilarList extends ArrayList<Double> {

    @Override
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < this.size(); i++) {
                if (get(i) == null) {
                    return i;
                }
            }
        } else {
            for (int i = 0; i < this.size(); i++) {
                if (almostEquals((Double)o, this.get(i))) {
                    return i;
                }
            }
        }
        return -1;
    }

    private boolean almostEquals(Double a, Double b) {
        long abits = Double.doubleToLongBits(a);
        long bbits = Double.doubleToLongBits(b);

        // Handle +0 == -0
        if((abits >> 63) != (bbits >> 63)) {
            return a.equals(b);
        }

        long diff = Math.abs(abits - bbits);

        if(diff <= 1) {
            return true;
        }

        return false;
    }
}

使用此代码变得容易一些(双关语无意):

package com.michaelt.so.doub;

import java.util.ArrayList;

public class ListTest {
    public static void main(String[] args) {
        ArrayList foo = new SimilarList();

        foo.add(1.38);
        foo.add(1.40);
        foo.add(0.02);
        foo.add(0.20);

        System.out.println(foo.contains(0.0));
        System.out.println(foo.contains(1.37 + 0.01));
        System.out.println(foo.contains(1.39 + 0.01));
        System.out.println(foo.contains(0.19 + 0.01));
        System.out.println(foo.contains(0.01 + 0.01));
    }
}

此代码的输出为:

false
true
true
true
true

在这种情况下,根据代码HasMinimalDifference在类似列表中完成位调整。同样,数字被转换为位,但这次数学是在比较中完成的,而不是尝试使用包装器对象的相等和哈希代码。

答案 3 :(得分:0)

  

背景:浮点数有舍入问题,因此不应将它们与&#34; ==&#34;进行比较。

这是错误的。编写浮点代码时需要保持清醒,但在很多情况下,推理程序中可能存在的错误很简单。如果你不能做到这一点,你至少应该对你的计算值有多么错误进行经验估计,并考虑你所看到的错误是否可以接受的小。

这意味着您无法避免弄脏手,并考虑您的计划正在做什么。如果您打算进行近似比较,您需要有一些想法什么差异意味着两个值确实不同,哪些差异意味着两个数量可能是相同的。

  

//应为1.38,但最终为1.3800000000000001

这也是错误的。请注意,最接近的double1.370x1.5eb851eb851ecp+0,而最近的double0.010x1.47ae147ae147bp-7。添加这些内容后,您会获得0x1.6147ae147ae14f6p+0,其转向0x1.6147ae147ae15p+0。距离double最近的1.380x1.6147ae147ae14p+0

有几个原因导致两个略有不同的double不比较==。这是两个:

  • 如果他们这样做,那就会打破传递性。 (abca == bb == c,但!(a == c)
  • 如果他们这样做,精心编写的数字代码就会停止工作。

尝试在列表中找到double真实问题是NaN不会将==与自身进行比较。您可以尝试使用检查needle == haystack[i] || needle != needle && haystack[i] != haystack[i]

的循环