title: 从一行代码到Guava DoubleMath tags:
- Java
- Guava
- NaN
- DoubleMath
- fuzzyEquals categories: guava date: 2017-12-05 22:09:37
上一篇我们看到如上问题
背景
现在项目中还有如下问题
问题
如下描述
Floating point math is imprecise because of the challenges of storing such values in a binary representation. Even worse, floating point math is not associative; push a
float
or adouble
through a series of simple mathematical operations and the answer will be different based on the order of those operation because of the rounding that takes place at each step.Even simple floating point assignments are not simple:
float f = 0.1; // 0.100000001490116119384765625 double d = 0.1; // 0.1000000000000000055511151231257827021181583404541015625 复制代码
(Results will vary based on compiler and compiler settings);
Therefore, the use of the equality (
==
) and inequality (!=
) operators onfloat
ordouble
values is almost always an error, and the use of other comparison operators (>
,>=
,<
,<=
) is also problematic because they don't work properly for -0 andNaN
.Instead the best course is to avoid floating point comparisons altogether. When that is not possible, you should consider using one of Java's float-handling
Numbers
such asBigDecimal
which can properly handle floating point comparisons. A third option is to look not for equality but for whether the value is close enough. I.e. compare the absolute value of the difference between the stored value and the expected value against a margin of acceptable error. Note that this does not cover all cases (NaN
andInfinity
for instance).This rule checks for the use of direct and indirect equality/inequailty tests on floats and doubles.
Noncompliant Code Example
float myNumber = 3.146; if ( myNumber == 3.146f ) { //Noncompliant. Because of floating point imprecision, this will be false // ... } if ( myNumber != 3.146f ) { //Noncompliant. Because of floating point imprecision, this will be true // ... } if (myNumber < 4 || myNumber > 4) { // Noncompliant; indirect inequality test // ... } float zeroFloat = 0.0f; if (zeroFloat == 0) { // Noncompliant. Computations may end up with a value close but not equal to zero. } 复制代码
测试
Double d1=Double.NaN;sout(d1==d1);
double d2=Double.NaN;sout(d2==d2);
复制代码
答案
结论
可以看到我们对于基本工具的封装仍然会有一些问题
我的原则是需要工具类优先从Guava中查找如果没有Apache commons等已经饱受考验的第三方库使用
比如关于double或者float的比较通畅会考虑到存在精度
因此我们可以使用DoubleMath
/**
* Returns {@code true} if {@code a} and {@code b} are within {@code tolerance} of each other.
*
* <p>Technically speaking, this is equivalent to
* {@code Math.abs(a - b) <= tolerance || Double.valueOf(a).equals(Double.valueOf(b))}.
*
* <p>Notable special cases include:
* <ul>
* <li>All NaNs are fuzzily equal.
* <li>If {@code a == b}, then {@code a} and {@code b} are always fuzzily equal.
* <li>Positive and negative zero are always fuzzily equal.
* <li>If {@code tolerance} is zero, and neither {@code a} nor {@code b} is NaN, then
* {@code a} and {@code b} are fuzzily equal if and only if {@code a == b}.
* <li>With {@link Double#POSITIVE_INFINITY} tolerance, all non-NaN values are fuzzily equal.
* <li>With finite tolerance, {@code Double.POSITIVE_INFINITY} and {@code
* Double.NEGATIVE_INFINITY} are fuzzily equal only to themselves.
* </li>
*
* <p>This is reflexive and symmetric, but <em>not</em> transitive, so it is <em>not</em> an
* equivalence relation and <em>not</em> suitable for use in {@link Object#equals}
* implementations.
*
* @throws IllegalArgumentException if {@code tolerance} is {@code < 0} or NaN
* @since 13.0
*/
public static boolean fuzzyEquals(double a, double b, double tolerance) {
MathPreconditions.checkNonNegative("tolerance", tolerance);
return
Math.copySign(a - b, 1.0) <= tolerance
// copySign(x, 1.0) is a branch-free version of abs(x), but with different NaN semantics
|| (a == b) // needed to ensure that infinities equal themselves
|| (Double.isNaN(a) && Double.isNaN(b));
}
复制代码
我们可以定义一个默认精度在我们自己的工具类中调用如上方法!