如何使用 Java 的 BigDecimal 正确计算?

How do I calculate correctly with Java’s BigDecimal?

据我了解,BigDecimal 用于正确处理具有固定小数位的数字(即金钱)。这是我写的一个小程序:

import java.math.*;

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(11.22, new MathContext(2, RoundingMode.HALF_UP));
        System.out.println("a: " + a);
        BigDecimal b = a.add(new BigDecimal(0.04));
        System.out.println("b: " + b);
    }
}

我希望看到:

a: 11.22
b: 11.26

但我得到的是:

a: 11
b: 11.040000000000000000832667268468867405317723751068115234375

我将 a 设置为有两位小数,但它都不会打印它们,甚至会忘记它们并四舍五入为纯整数。为什么? b 应该加 0.04 并且从 a 知道也有两位小数。这至少是我所期望的。

如何使用 BigDecimal edit: 将两个双精度值作为输入并已知小数位数来正确解决这个问题? [即,因为 API 除了这两个双打之外没有给我任何其他东西。](我知道还有其他方法可以可靠地用钱计算(从用 [=18= 计算美分开始) ]), 但这超出了我的问题范围。)

不要使用 BigDecimal constructor with the double argument 正是因为这个原因:

  • The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

使用constructor with the String argument:

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("11.22");
    System.out.println("a: " + a);
    BigDecimal b = a.add(new BigDecimal("0.04"));
    System.out.println("b: " + b);
}

这将按预期生成输出:

a: 11.22
b: 11.26

正如@Progman 所写,使用 double 构造函数将创建 double 值的精确十进制表示,它是 advised against in the documentation

然而,您得到 11 而不是 11.22 的原因是您已将 MathContext 的精度设置为 2。
精度是使用的位数,而不是小数位数。因此,如果您将代码更改为使用 4 作为精度,那么您将获得输出

a: 11.22
b: 11.260000000000000000832667268468867405317723751068115234375

仍然存在使用双精度值的问题,但现在有更多的小数位!

精度作为位数的定义在MathContext class documentation文档

使用双精度值而不是字符串作为 BigDecimal 的参数的问题在于它们大多不准确,并且会导致重复的小数扩展。事实上,只有分母中 5 和或 2 的幂的浮点数才能精确表示为浮点数或双精度数(例如 1/20 = .05 1/4 = .25 , 1/5 = .2)。这是因为 52 是基数 10 的唯一质因数,并且 return 将是分数的有限(即 non-repeating)展开。任何其他值都会导致重复的小数扩展(例如 1/3 = .3333333333、1/6 = .16666666),这就是导致您出现问题的原因。

通过指定字符串而不是双精度数,BigDecimal 可以对预期的期望值进行操作,而不是它限制或无法控制的二进制值。

如果您的价值观如下。

BigDecimal a = new BigDecimal(11.25);             
System.out.println("a: " + a);
BigDecimal b = a.add(new BigDecimal(.50));
System.out.println("b: " + b);

输出本来是

11.25
11.75

因为小数部分 结果总和只有 5 和 2 作为它们的约数。

因此,您应该在初始化 BigDecimal 对象时指定浮点值的字符串表示形式。

有关浮点数内部表示的更多信息,请查看IEEE 754