1. Overview
1.概述
In this tutorial, we’ll talk about the different ways of comparing double values in Java. In particular, it isn’t as easy as comparing other primitive types. As a matter of fact, it’s problematic in many other languages, not only Java.
在本教程中,我们将讨论在Java中比较双倍值的不同方法。特别是,它不像比较其他原始类型那样容易。事实上,这在许多其他语言中都是有问题的,不仅仅是Java。
First, we’ll explain why using the simple == operator is inaccurate and might cause difficult to trace bugs in the runtime. Then, we’ll show how to compare doubles in plain Java and common third-party libraries correctly.
首先,我们将解释为什么使用简单的==运算符是不准确的,并可能导致在运行时难以追踪的错误。然后,我们将展示如何在普通Java和常见的第三方库中正确比较双数。
2. Using the == Operator
2.使用==运算符
Inaccuracy with comparisons using the == operator is caused by the way double values are stored in a computer’s memory. We need to remember that there is an infinite number of values that must fit in limited memory space, usually 64 bits. As a result, we can’t have an exact representation of most double values in our computers. They must be rounded to be saved.
使用==运算符进行比较的不精确性是由计算机内存中存储双倍值的方式造成的。我们需要记住,在有限的内存空间(通常是64位)中必须容纳无限多的值。因此,我们无法在计算机中准确地表示大多数双倍值。它们必须经过四舍五入才能被保存。
Because of the rounding inaccuracy, interesting errors might occur:
由于四舍五入的不精确性,可能会出现有趣的错误。
double d1 = 0;
for (int i = 1; i <= 8; i++) {
d1 += 0.1;
}
double d2 = 0.1 * 8;
System.out.println(d1);
System.out.println(d2);
Both variables, d1 and d2, should equal 0.8. However, when we run the code above, we’ll see the following results:
两个变量,d1和d2,都应该等于0.8。然而,当我们运行上面的代码时,我们会看到以下结果。
0.7999999999999999
0.8
In that case, comparing both values with the == operator would produce a wrong result. For this reason, we must use a more complex comparison algorithm.
在这种情况下,用==运算符比较两个值会产生一个错误的结果。由于这个原因,我们必须使用一个更复杂的比较算法。
If we want to have the best precision and control over the rounding mechanism, we can use java.math.BigDecimal class.
如果我们想拥有最好的精度和对舍入机制的控制,我们可以使用java.math.BigDecimal类。
3. Comparing Doubles in Plain Java
3.在普通的Java中比较双数
The recommended algorithm to compare double values in plain Java is a threshold comparison method. In this case, we need to check whether the difference between both numbers is within the specified tolerance, commonly called epsilon:
在普通Java中比较双倍值的推荐算法是阈值比较法。在这种情况下,我们需要检查两个数字之间的差异是否在指定的公差范围内,通常称为epsilon:。
double epsilon = 0.000001d;
assertThat(Math.abs(d1 - d2) < epsilon).isTrue();
The smaller the epsilon’s value, the greater the comparison accuracy. However, if we specify the tolerance value too small, we’ll get the same false result as in the simple == comparison. In general, epsilon’s value with 5 and 6 decimals is usually a good place to start.
epsilon的值越小,比较的准确性就越高。然而,如果我们指定的公差值太小,我们会得到和简单的==比较中一样的错误结果。一般来说,epsilon的值有5和6位小数,通常是一个好的开始。
Unfortunately, there is no utility from the standard JDK that we could use to compare double values in the recommended and precise way. Luckily, we don’t need to write it by ourselves. We can use a variety of dedicated methods provided by free and widely known third-party libraries.
不幸的是,在标准的JDK中没有任何工具可以让我们用推荐的、精确的方式来比较双倍值。幸运的是,我们不需要自己编写它。我们可以使用由免费和广为人知的第三方库提供的各种专用方法。
4. Using Apache Commons Math
4.使用Apache Commons数学
Apache Commons Math is one of the biggest open-source library dedicated to mathematics and statistics components. From the variety of different classes and methods, we’ll focus on org.apache.commons.math3.util.Precision class in particular. It contains 2 helpful equals() methods to compare double values correctly:
Apache Commons Math是致力于数学和统计组件的最大开源库之一。从各种不同的类和方法中,我们将特别关注org.apache.commons.math3.util.Precision类。它包含2个有用的equals()方法来正确比较双倍值。
double epsilon = 0.000001d;
assertThat(Precision.equals(d1, d2, epsilon)).isTrue();
assertThat(Precision.equals(d1, d2)).isTrue();
The epsilon variable used here has the same meaning as in the previous example. It is an amount of allowed absolute error. However, it’s not the only similarity to the threshold algorithm. In particular, both equals methods use the same approach under the hood.
这里使用的epsilon变量与前一个例子中的含义相同。它是一个允许绝对误差的量。然而,这并不是唯一与阈值算法相似的地方。特别是,两个equals方法都在引擎盖下使用相同的方法。
The two-argument function version is just a shortcut for the equals(d1, d2, 1) method call. In this case, d1 and d2 are considered equal if there are no floating point numbers between them.
双参数函数版本只是equals(d1, d2, 1)方法调用的一个快捷方式。在这种情况下,如果d1和d2之间没有浮点数,就认为它们相等。
5. Using Guava
5.使用番石榴
Google’s Guava is a big set of core Java libraries that extend the standard JDK capabilities. It contains a big number of useful math utils in the com.google.common.math package. To compare double values correctly in Guava, let’s implement the fuzzyEquals() method from the DoubleMath class:
Google的Guava是一大套核心Java库,它扩展了标准的JDK功能。它在com.google.common.math包中包含了大量有用的数学工具。为了在Guava中正确地比较双倍值,让我们从DoubleMath类中实现fuzzyEquals()方法:
double epsilon = 0.000001d;
assertThat(DoubleMath.fuzzyEquals(d1, d2, epsilon)).isTrue();
The method name is different than in the Apache Commons Math, but it works practically identically under the hood. The only difference is that there is no overloaded method with the epsilon’s default value.
该方法的名称与Apache Commons Math中的不同,但它的工作原理实际上是相同的。唯一不同的是,没有重载的方法,没有epsilon的默认值。
6. Using JUnit
6.使用JUnit
JUnit is one of the most widely used unit testing frameworks for Java. In general, every unit test usually ends with analyzing the difference between expected and actual values. Therefore, the testing framework must have correct and precise comparison algorithms. In fact, JUnit provides a set of comparing methods for common objects, collections, and primitive types, including dedicated methods to check double values equality:
JUnit是Java中最广泛使用的单元测试框架之一。一般来说,每个单元测试通常以分析预期值和实际值之间的差异而结束。因此,测试框架必须有正确和精确的比较算法。事实上,JUnit为常见的对象、集合和原始类型提供了一套比较方法,包括检查双倍值是否相等的专门方法。
double epsilon = 0.000001d;
assertEquals(d1, d2, epsilon);
As a matter of fact, it works the same as Guava’s and Apache Commons’s methods previously described.
事实上,它的工作原理与之前描述的Guava和Apache Commons的方法相同。
It’s important to point out that there is also a deprecated, two-argument version without the epsilon argument. However, if we want to be sure our results are always correct, we should stick with the three-argument version.
需要指出的是,还有一个废弃的、没有ε参数的双参数版本。然而,如果我们想确保我们的结果总是正确的,我们应该坚持使用三个参数的版本。
7. Conclusion
7.结语
In this article, we’ve explored different ways of comparing double values in Java.
在这篇文章中,我们已经探讨了在Java中比较双倍值的不同方法。
We’ve explained why simple comparison might cause difficult to trace bugs in the runtime. Then, we’ve shown how to compare values in plain Java and common libraries correctly.
我们已经解释了为什么简单的比较可能会在运行时造成难以追踪的错误。然后,我们展示了如何正确比较纯Java和普通库中的值。
As always, the source code for the examples can be found over on GitHub.
一如既往,这些例子的源代码可以在GitHub上找到。