1. 概述

在 Java 中,对 double 类型进行精确比较并不是一件直观的事情。不像整型可以直接使用 == 判断相等性,浮点数由于其存储机制的特殊性,会导致使用 == 进行比较时出现难以预料的结果。这个问题不仅存在于 Java,也是其他编程语言中常见的陷阱。

在本文中,我们会解释为什么简单的 == 比较会出错,并介绍如何在 Java 原生环境和常用第三方库中正确地比较 double 值。

2. 使用 == 运算符的问题

浮点数在计算机中的表示是基于 IEEE 754 标准的,由于内存是有限的(通常为 64 位),无法精确表示所有的小数。因此,很多浮点数在存储时必须经过舍入处理,这就引入了误差。

来看一个经典例子:

double d1 = 0;
for (int i = 1; i <= 8; i++) {
    d1 += 0.1;
}

double d2 = 0.1 * 8;

System.out.println(d1);  // 输出:0.7999999999999999
System.out.println(d2);  // 输出:0.8

虽然 d1d2 在数学上应该都等于 0.8,但由于浮点精度问题,实际值存在微小差异。如果此时使用 == 来比较,结果会是 false,这会导致程序逻辑出错。

踩坑提示:永远不要直接用 == 比较两个浮点数。

3. Java 原生方式:使用阈值比较(Epsilon)

为了解决这个问题,推荐使用“阈值比较”方法,即判断两个浮点数的差值是否小于一个极小值(称为 epsilon)。

double epsilon = 0.000001d;

assertThat(Math.abs(d1 - d2) < epsilon).isTrue();
  • epsilon 越小,精度越高。
  • ⚠️ 如果 epsilon 设置得太小,可能仍然无法避免精度误差带来的误判。
  • 💡 通常建议从 5~6 位小数开始尝试,如 0.000001

不过,JDK 并没有内置专门用于浮点数比较的方法,因此我们通常会借助第三方库来简化操作。

4. 使用 Apache Commons Math 库

Apache Commons Math 是一个专注于数学和统计计算的开源库。它提供了 org.apache.commons.math3.util.Precision 类,其中包含两个实用的 equals() 方法用于浮点比较:

double epsilon = 0.000001d;

assertThat(Precision.equals(d1, d2, epsilon)).isTrue();
assertThat(Precision.equals(d1, d2)).isTrue();
  • 第一个方法允许自定义 epsilon
  • 第二个方法是默认版本,等价于 Precision.equals(d1, d2, 1),即判断两个数之间是否没有其他浮点数。

✅ 这两个方法底层都采用了与手动阈值比较相同的算法。

5. 使用 Google Guava 库

Guava 是 Google 提供的一套增强 Java 核心功能的工具库。它提供了 com.google.common.math.DoubleMath 类,其中包含 fuzzyEquals() 方法:

double epsilon = 0.000001d;

assertThat(DoubleMath.fuzzyEquals(d1, d2, epsilon)).isTrue();
  • ✅ 功能与 Apache Commons Math 类似。
  • ❌ 不支持默认 epsilon,必须手动传入。

6. 使用 JUnit 进行测试断言

在单元测试中,经常需要比较浮点数是否相等。JUnit 提供了专门用于浮点数比较的断言方法:

double epsilon = 0.000001d;
assertEquals(d1, d2, epsilon);
  • ✅ 与 Guava 和 Apache Commons Math 的机制一致。
  • ⚠️ 注意:assertEquals(double, double)(无 epsilon)已被标记为 deprecated,应始终使用三参数版本。

7. 使用 Comparator 比较器

我们也可以通过实现 Comparator<Double> 接口来自定义浮点数比较逻辑:

public class DoubleComparator implements Comparator<Double> {
    private double epsilon;

    public DoubleComparator(double epsilon) {
        this.epsilon = epsilon;
    }

    @Override
    public int compare(Double d1, Double d2) {
        if (Math.abs(d1 - d2) < epsilon) {
            return 0; // 相等
        } else if (d1 < d2) {
            return -1; // 小于
        } else {
            return 1; // 大于
        }
    }
}

测试代码如下:

DoubleComparator comparator = new DoubleComparator(0.000001d);
int result = comparator.compare(d1, d2);
assertEquals(0, result);

✅ 适用于需要排序或自定义比较规则的场景。

8. 按小数位数比较

在某些业务场景中(如金融计算),我们可能只需要比较到小数点后几位是否一致。此时可以借助 BigDecimal

boolean areEqual(double d1, double d2, int decimalPlaces) {
    BigDecimal bd1 = BigDecimal.valueOf(d1).setScale(decimalPlaces, RoundingMode.HALF_UP);
    BigDecimal bd2 = BigDecimal.valueOf(d2).setScale(decimalPlaces, RoundingMode.HALF_UP);
    return bd1.equals(bd2);
}

测试示例:

double d1 = 0.7999999999999999;
double d2 = 0.8;

assertTrue(areEqual(d1, d2, 1), "Should be equal up to 1 decimal place");
assertTrue(areEqual(d1, d2, 2), "Should be equal up to 2 decimal places");

✅ 适合需要“显示精度”一致性的场景,比如金额、报表等。

9. 总结

本文介绍了 Java 中比较 double 类型值的多种方式:

  • ❌ 避免直接使用 ==
  • ✅ 推荐使用 epsilon 阈值比较
  • 🧰 可借助 Apache Commons Math、Guava、JUnit 等库提供的方法
  • 📊 在特定场景下,可以使用 BigDecimal 或自定义 Comparator

浮点数比较虽小,却是程序健壮性的重要一环,务必谨慎处理。


原始标题:Comparing Doubles in Java | Baeldung