如何在Java中正确使用BigDecimal进行计算?

huangapple go评论71阅读模式
英文:

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 解决这个问题呢?***编辑:***输入为两个双精度浮点数,并且已知小数位数?(我知道有其他可靠地处理货币计算的方法(例如从以“分”为单位进行计算,使用 int 类型),但这超出了我问题的范围。)

英文:

As I understood it, BigDecimal is there for properly dealing with numbers with fixed decimal places (i.e. money). Here is a little program I wrote:

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);
    }
}

I expected to see:

a: 11.22
b: 11.26

But what I got is:

a: 11
b: 11.040000000000000000832667268468867405317723751068115234375

I set a to have two decimal places, but it both does not print them, and it even forgets them and rounds to plain int. Why? b should add 0.04 and know from a to have two decimal places, too. This was at least what I expected.

How is this solved correctly using BigDecimal edit: with two double values as input and a known number of decimal places? [I.e., because an API does not give me anything else than these two doubles.] (I know there are other ways to reliably calculate with money (starting from calculating in cents with int), but that’s out of the scope of my question.)

答案1

得分: 4

不要使用带有double参数的BigDecimal构造函数,就是因为这个原因:

  • 这个构造函数的结果可能有些不可预测。有人可能会认为在Java中写入new BigDecimal(0.1)会创建一个精确等于0.1的BigDecimal(无标度值为1,标度为1),但实际上它等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1不能精确地表示为double(或者可以说,不能精确地表示为任何有限长度的二进制分数)。因此,传递给构造函数的值与0.1并不完全相等,尽管外表如此。

请使用带有String参数的构造函数

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
英文:

Do not use the BigDecimal constructor with the double argument for exactly that reason:

> * 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.

Use the 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);
}

This will generate the output as expected:

a: 11.22
b: 11.26

答案2

得分: 1

如@Progman所写,使用双重构造函数将创建double值的精确十进制表示,而根据文档建议,不建议这样做。

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

a: 11.22
b: 11.260000000000000000832667268468867405317723751068115234375

问题仍然是使用double值,但现在小数位数更多!

有关将精度定义为数字位数的定义在MathContext类文档中。

英文:

As @Progman also wrote, using the double constructor will create an exact decimal representation of the double value, and it is advised against in the documentation.

However the reason you are getting 11 instead of 11.22 is that you have set the precision of your MathContext to 2.
Precision is the number of digits used and not the number of decimal places. So if you changed your code to use 4 as the precision, then you would get the output

a: 11.22
b: 11.260000000000000000832667268468867405317723751068115234375

Still with the problem of the double value being used, but now with more decimal places!

The definition of the precision as the number of digits is in the MathContext class documentationdocumentation

答案3

得分: 1

使用双精度浮点数(double values)作为BigDecimal的参数而不是字符串(strings)的问题在于,它们大多数情况下并不精确,并且会导致重复的小数展开。事实上,只有分母为5和/或2的浮点数幂可以被准确地表示为浮点数或双精度浮点数(例如,1/20 = 0.05,1/4 = 0.25,1/5 = 0.2)。这是因为52是基数10的唯一素因子,将返回分数的有限(即非重复)展开。任何其他值都将导致循环小数展开(例如,1/3 = 0.3333333333,1/6 = 0.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

英文:

The problem with using double values instead of strings as argument to BigDecimal is that they are mostly not exact and will result in repeating decimal expansions. In fact, only floating point numbers that are powers of 5 and or 2 in the denominator can be represented exactly as a float or double (e.g 1/20 = .05 1/4 = .25 , 1/5 = .2). That is because 5 and 2 are the only prime factors of base 10 and will return a finite (i.e. non-repeating) expansion of the fraction. Any other value will result in a repeating decimal expansion (e.g. 1/3 = .3333333333, 1/6 = .16666666), and this is what is causing your problem.

By specifying Strings instead of doubles, BigDecimal can operate on the expected desired value as opposed to the binary value to which it has limited or no control over.

Had your values been the following.

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

The output would have been

11.25
11.75

Because both fractional parts and the resulting sum have only 5 and 2 as their divisors.

For this reason, you should specify string representation of floating point values when initializing BigDecimal objects.

For more about internal representation of floating point numbers, check out IEEE 754

huangapple
  • 本文由 发表于 2020年10月26日 21:15:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/64537877.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定