背景:浮点数有舍入问题,因此不应将它们与" =="进行比较。
问题:在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
谢谢。
答案 0 :(得分:4)
一般的解决方案是编写一个循环遍历列表的实用程序方法,并检查每个元素是否在目标值的某个阈值内。不过,我们可以使用Stream#anyMatch()
:
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
这也是错误的。请注意,最接近的double
到1.37
为0x1.5eb851eb851ecp+0
,而最近的double
到0.01
为0x1.47ae147ae147bp-7
。添加这些内容后,您会获得0x1.6147ae147ae14f6p+0
,其转向0x1.6147ae147ae15p+0
。距离double
最近的1.38
是0x1.6147ae147ae14p+0
。
有几个原因导致两个略有不同的double
不比较==
。这是两个:
a
,b
和c
会a == b
和b == c
,但!(a == c)
。尝试在列表中找到double
的真实问题是NaN
不会将==
与自身进行比较。您可以尝试使用检查needle == haystack[i] || needle != needle && haystack[i] != haystack[i]
。