在加法之前将双精度转换为 BigDecimal 以保持原始精度是否足够?

Is it sufficient to convert a double to a BigDecimal just before addition to retain original precision?

提问人:bubblefoil 提问时间:1/3/2019 最后编辑:bubblefoil 更新时间:1/3/2019 访问量:1369

问:

我们正在解决一个与数字精度相关的错误。我们的系统收集一些数字并吐出它们的总和。 问题是系统没有保留数字精度,例如 300.7 + 400.9 = 701.599...,而预期结果为 701.6。精度应该适应输入值,因此我们不能只是将结果四舍五入到固定精度。

问题很明显,我们使用 double 作为值,加法会从十进制值的二进制表示中累积误差。

数据的路径如下:

  1. XML 文件,键入 xsd:decimal
  2. 解析为 java 基元双精度。它的 15 位小数应该足够了,我们期望值总计不超过 10 位,5 位小数。
  3. 存储到数据库 MySql 5.5 中,键入 double
  4. 通过 Hibernate 加载到 JPA 实体中,即仍然是原始的 double
  5. 这些值的总和
  6. 将总和打印到另一个 XML 文件中

现在,我认为最佳解决方案是将所有内容转换为十进制格式。不出所料,使用最便宜的解决方案存在压力。事实证明,在添加几个数字之前将双精度转换为BigDecimal在以下示例中的情况B中有效:

import java.math.BigDecimal;

public class Arithmetic {
    public static void main(String[] args) {
        double a = 0.3;
        double b = -0.2;
        // A
        System.out.println(a + b);//0.09999999999999998
        // B
        System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
        // C
        System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
    }
}

关于这一点的更多信息:为什么我们需要将 double 转换为字符串,然后才能将其转换为 BigDecimal? BigDecimal(double) 构造函数的不可预测性

我担心这样的解决方法会是一颗定时炸弹。 首先,我不太确定这个算术是否适用于所有情况。 其次,仍然存在一些风险,将来有人可能会实施一些更改并将 B 更改为 C,因为这个陷阱远非显而易见,即使是单元测试也可能无法揭示错误。

我愿意接受第二点,但问题是:这种解决方法会提供正确的结果吗?有没有一种情况,不知何故

Double.valueOf("12345.12345").toString().equals("12345.12345")

是假的吗?根据 javadoc 的说法,鉴于 Double.toString 只打印唯一表示底层双精度值所需的数字,那么当再次解析时,它会给出相同的双精度值?对于这个用例来说,这还不够吗,我只需要用这个神奇的 Double.toString(Double d) 方法添加数字并打印总和?需要明确的是,我确实更喜欢我认为的干净解决方案,到处使用 BigDecimal,但我有点缺乏论据来推销它,我的意思是理想情况下,在加法之前转换为 BigDecimal 无法完成上述工作。

Java 双精度 BigDecimal

评论

2赞 Andy Turner 1/3/2019
“2. 解析成 java 基元双精度” 当你这样做时,你已经失去了精度。直接将其解析为 .BigDecimal
1赞 Compass 1/3/2019
如果您需要保证精度,则不应在加工的任何部分使用。double
2赞 RealSkeptic 1/3/2019
在将数据解析为 .a 无法精确地包含该值。它根本无法以二进制格式表示。但它是“正确”打印的,因为字符串转换考虑到了这一点并对其进行了舍入。这就是为什么直接转换是不可预测的。doubledouble0.2double
0赞 Louis Wasserman 1/3/2019
他们说:你触摸的那一刻,它就坏了。仅 BigDecimals,并且只有在直接从 Strings: 转换时才真正如此: ,而不是 .doubleBigDecimal.valueOf("0.3")BigDecimal.valueOf(0.3)
0赞 bubblefoil 1/3/2019
是的,浮点格式在内部并不精确地表示十进制值。无论如何,double 足够精确,可以容纳一个 15 位数字,因此,例如,当 10 位值从 String 解析为 double,使用 Double.toString 中的特殊舍入算法再次打印到 String 时,我应该产生与我最初解析 double 相同的值,对吧?然后,这个 String 用于构造 BigDecimal,而 BigDecimal 又用于数学运算。我错过了其他一些陷阱?

答:

0赞 Ashok 1/3/2019 #1

It depends on the Database you are using. If you are using SQL Server you can use data type as numeric(12, 8) where 12 represent numeric value and 8 represents precision. similarly, for my SQL DECIMAL(5,2) you can use.

You won't lose any precision value if you use the above-mentioned datatype.

Java Hibernate Class :

You can define private double latitude;

Database:

Database Design

评论

0赞 RealSkeptic 1/3/2019
You are ignoring the fact that the number was parsed into in the first place, thus lost precision already. The type to use in the database is less important - all databases have a way of representing exact decimal numbers.double
0赞 Peter Lawrey 1/3/2019
@RealSkeptic when converted to a String you can reliably get the decimal you started with depending on how many digits you need.
2赞 Ralf Kleberhoff 1/3/2019 #2

If you can't avoid parsing into primitive or store as double, you should convert to as early as possible.doubleBigDecimal

double can't exactly represent decimal fractions. The value in will never be exactly 7.3, but something very very close to it, with a difference visible from the 16th digit or so on to the right (giving 50 decimal places or so). Don't be mislead by the fact that printing might give exactly "7.3", as printing already does some kind of rounding and doesn't show the number exactly.double x = 7.3;

If you do lots of computations with numbers, the tiny differences will eventually sum up until they exceed your tolerance. So using doubles in computations where decimal fractions are needed, is indeed a ticking bomb.double

[...] we expect values no longer than 10 digits total, 5 fraction digits.

I read that assertion to mean that all numbers you deal with, are to be exact multiples of , without any further digits. You can convert doubles to such BigDecimals with 0.00001

new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)

This will give you an exact representation of a number with 5 decimal fraction digits, the 5-fraction-digits one that's closest to the input . This way you correct for the tiny differences between the and the decimal number that you originally meant.doubleValdoubleVal

If you'd simply use , you'd go through the string representation of the double you're using, which can't guarantee that it's what you want. It depends on a rounding process inside the Double class which tries to represent the double-approximation of 7.3 (being maybe 7.30000000000000123456789123456789125) with the most plausible number of decimal digits. It happens to result in "7.3" (and, kudos to the developers, quite often matches the "expected" string) and not "7.300000000000001" or "7.3000000000000012" which both seem equally plausible to me.BigDecimal.valueOf(double val)

That's why I recommend not to rely on that rounding, but to do the rounding yourself by decimal shifting 5 places, then rounding to the nearest long, and constructing a BigDecimal scaled back by 5 decimal places. This guarantees that you get an exact value with (at most) 5 fractional decimal places.

Then do your computations with the BigDecimals (using the appropriate MathContext for rounding, if necessary).

When you finally have to store the number as a double, use . The resulting double will be close enough to the decimal that the above-mentioned conversion will surely give you the same BigDecimal that you had before (unless you have really huge numbers like 10 digits before the decimal point - the you're lost with anyway).BigDecimal.doubleValue()double

P.S. Be sure to use only if decimal fractions are relevant to you - there were times when the British Shilling currency consisted of twelve Pence. Representing fractional Pounds as would give a disaster much worse than using doubles.BigDecimalBigDecimal

评论

0赞 Rudy Velthuis 1/3/2019
Very well explained. I'd +2 if I could. <g>
0赞 Ralf Kleberhoff 1/3/2019
Thanks, very kind of you :-)
0赞 bubblefoil 1/3/2019
Thanks for your elaboration! Why is it necessary to shift the decimal point before the conversion? According to javadoc and a test with a couple of arbitrary numbers, BigDecimal.valueOf, should provide the correct value, though I cannot really test all the numbers :-) BTW I assume you suggest using the factory method and the keyword is there by a mistake.new