为什么增加0.1倍仍然是无损的?

我知道 0.1十进制数不能精确地用有限的二进制数(解释)来表示,所以 double n = 0.1会失去一些精度,不能精确地表示为 0.1。另一方面,0.5可以精确地表示,因为它是 0.5 = 1/2 = 0.1b

说了这么多,可以理解,加入 0.1 三次不会得到完全的 0.3,所以下面的代码打印出 false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
sum += d;
System.out.println(sum == 0.3); // Prints false, OK

但是为什么加上 0.1 五次就能得到 0.5呢? 下面的代码打印出 true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

如果 0.1不能精确地表示,那么将它加5次怎么能得到精确地表示的 0.5呢?

17391 次浏览

舍入误差不是随机的,它的实现方式试图使误差最小化。这意味着有时候错误是不可见的,或者没有错误。

例如,0.1不完全是 0.1,即 new BigDecimal("0.1") < new BigDecimal(0.1),而 0.5恰好是 1.0/2

这个程序向您展示了所涉及的真正价值。

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
x = x.add(_0_1);
}

指纹

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

注意: 0.3稍有偏差,但是当你到达 0.4时,位必须向下移动一才能满足53位的限制,这个错误就被丢弃了。同样,对于 0.60.7,错误悄悄地返回,但是对于 0.81.0,错误被丢弃。

添加5次应该会累积错误,而不是取消它。

产生误差的原因是由于精度有限。即53位。这意味着随着数字越来越大,使用的比特越来越多,比特必须从末端丢弃。这会导致四舍五入,在这种情况下,四舍五入对你有利。
你可以得到相反的效果,当得到一个较小的数字,例如 0.1-0.0999 = > 1.0000000000000286E-4 你会看到比以前更多的错误。

这方面的一个例子就是为什么在 Java6为什么 Math.round (0.4999999999999999999994)返回1中,在这种情况下,计算中的一点损失会导致答案的很大差异。

除非溢出,在浮点数中,x + x + x正好是正确舍入的(即最接近的)浮点数,实数3 * xx + x + x + x正好是4 * x,而 x + x + x + x + x又是正确舍入的5 * x的浮点近似值。

对于 x + x + x,第一个结果来自于 x + x是精确的这一事实。因此,x + x + x只是一个舍入的结果。

第二个结果比较困难,我们讨论了 给你的一个证明(Stephen Canon 提到了另一个关于 x最后3位数字的案例分析证明)。总之,3 * x与2 * x在相同的 手足无措中,或者与4 * x在相同的组合中,并且在每种情况下都可以推断出第三个加法上的错误抵消了第二个加法上的错误(正如我们已经说过的,第一个加法是精确的)。

第三个结果,“ x + x + x + x + x是正确舍入”,来自第二个,以同样的方式,第一个来自准确性的 x + x


第二个结果解释了为什么 0.1 + 0.1 + 0.1 + 0.1恰好是浮点数 0.4: 有理数1/10和4/10在转换为浮点数时得到了相同的近似值,具有相同的相对误差。这些浮点数之间的比率正好是4。第一和第三个结果表明,0.1 + 0.1 + 0.10.1 + 0.1 + 0.1 + 0.1 + 0.1的误差可能小于初始误差分析可能推断的误差,但是它们本身只将结果分别与 3 * 0.15 * 0.1联系起来,可以预期它们与 0.30.5接近但不一定相同。

如果你在第四次加法后继续加入 0.1,你将最终观察到舍入误差,使得“ 0.1加入自身 n 次”偏离 n * 0.1,甚至偏离 n/10更多。如果你将“0.1加到自身 n 次”的值作为 n 的函数,你会观察到由二进制组成的等斜率线(一旦第 n 次加法的结果注定要落入一个特定的二进制中,可以预期加法的性质类似于先前在同一二进制中产生结果的加法)。在同一个绑定中,错误将增大或收缩。如果您查看从绑定到绑定的斜率序列,您将在一段时间内识别二进制 0.1的重复数字。之后,吸收开始发生,曲线变平。

浮点数系统有各种神奇的功能,包括为舍入提供一些额外的精度。因此,由于0.1的不精确表示而导致的非常小的误差最终被四舍五入为0.5。

可以把浮点数看作是表示数字的一种很好但不精确的方法。并非所有可能的数字都能轻易地在计算机中表示出来。像 PI 这样的无理数。或者像 SQRT (2)。(符号数学系统可以表示它们,但我说的是“容易”。)

浮点值可能非常接近,但并不精确。它可能是如此接近,以至于你可以导航到冥王星,并偏离几毫米。但在数学意义上仍不精确。

当需要精确而非近似时,不要使用浮点数。例如,会计应用程序希望精确跟踪帐户中的一定数量的硬币。整数很好,因为它们是精确的。使用整数时需要注意的主要问题是溢出。

在货币中使用 BigDecimal 效果很好,因为基底形式是一个整数,尽管很大。

认识到浮点数并不精确,它们仍然有很多用途。图形系统中用于导航或坐标的坐标系统。天文学价值。科学价值观。(无论如何,你可能无法知道棒球的确切质量与电子的质量之比,所以不确切并不重要。)

对于计数应用程序(包括计数) ,使用整数。要计算通过一个门的人数,使用 int 或 long。