为什么双加有时正确,有时错?

我知道java有双精度陷阱,但为什么有时候,近似结果是可以的,但有时却不是。

像这样的代码:

for ( float value = 0.0f; value < 1.0f; value += 0.1f ) System.out.println( value ); 

结果是这样的:

 0.0 0.1 0.2 0.3 ... 0.70000005 0.8000001 0.9000001 

如您所述,并非所有数字都可以在IEEE754中完全表示。 结合Java用于打印这些数字的规则,这会影响您将看到的内容。

有关背景知识,我将简要介绍IEEE754的不准确之处。 在这种特殊情况下, 0.1无法准确表示,因此您经常会发现使用的实际数字类似于0.100000001490116119384765625

请参阅此处以分析为何如此。 您获得“不准确”值的原因是因为该错误( 0.000000001490116119384765625 )逐渐累加。


0.10.2 (或类似数字)的原因并不总是表明错误与Java中的打印代码有关,而不是实际值本身。

即使0.1实际上比你期望的高一点,打印出来的代码也不会给你所有的数字。 您会发现,如果您将格式字符串设置为在小数点后面提供50位数字,那么您将看到真正的值。

这里详细介绍了Java如何决定打印浮点数(没有显式格式化)的规则。 数字计数的相关位是:

必须至少有一个数字来表示小数部分,并且除此之外必须有多个,但只有多少个,更多的数字才能唯一地将参数值与float类型的相邻值区分开来。

举个例子,这里有一些代码告诉你这是如何工作的:

 public class testprog { public static void main (String s[]) { float n; int i, x; for (i = 0, n = 0.0f; i < 10; i++, n += 0.1f) { System.out.print( String.format("%30.29f %08x ", n, Float.floatToRawIntBits(n))); System.out.println (n); } } } 

这个输出是:

 0.00000000000000000000000000000 00000000 0.0 0.10000000149011611938476562500 3dcccccd 0.1 0.20000000298023223876953125000 3e4ccccd 0.2 0.30000001192092895507812500000 3e99999a 0.3 0.40000000596046447753906250000 3ecccccd 0.4 0.50000000000000000000000000000 3f000000 0.5 0.60000002384185791015625000000 3f19999a 0.6 0.70000004768371582031250000000 3f333334 0.70000005 0.80000007152557373046875000000 3f4cccce 0.8000001 0.90000009536743164062500000000 3f666668 0.9000001 

第一列是浮点数的实际值,包括IEEE754限制的不准确性。

第二列是浮点值的32位整数表示(它在内存中的外观而不是实际的整数值),用于检查低级别位表示的值。

最后一列是您在打印出没有格式化的数字时看到的内容。


现在看一些更多的代码,它们将向您展示如何不断添加不精确值的不准确性将给出错误的数字, 以及与周围值的差异如何控制打印的内容:

 public class testprog { public static void outLines (float n) { int i, val = Float.floatToRawIntBits(n); for (i = -1; i < 2; i++) { n = Float.intBitsToFloat(val+i); System.out.print( String.format("%30.29f %.08f %08x ", n, n, Float.floatToRawIntBits(n))); System.out.println (n); } System.out.println(); } public static void main (String s[]) { float n = 0.0f; for (int i = 0; i < 6; i++) n += 0.1f; outLines (n); n += 0.1f; outLines (n); n += 0.1f; outLines (n); n += 0.1f; outLines (0.7f); } } 

此代码使用0.1的连续添加以达到0.6然后打印出该值和相邻浮点数的值。 输出是:

 0.59999996423721310000000000000 0.59999996 3f199999 0.59999996 0.60000002384185790000000000000 0.60000002 3f19999a 0.6 0.60000008344650270000000000000 0.60000008 3f19999b 0.6000001 0.69999998807907100000000000000 0.69999999 3f333333 0.7 0.70000004768371580000000000000 0.70000005 3f333334 0.70000005 0.70000010728836060000000000000 0.70000011 3f333335 0.7000001 0.80000001192092900000000000000 0.80000001 3f4ccccd 0.8 0.80000007152557370000000000000 0.80000007 3f4cccce 0.8000001 0.80000013113021850000000000000 0.80000013 3f4ccccf 0.80000013 0.69999992847442630000000000000 0.69999993 3f333332 0.6999999 0.69999998807907100000000000000 0.69999999 3f333333 0.7 0.70000004768371580000000000000 0.70000005 3f333334 0.70000005 

首先要看的是,最后一列在每个块的中间行中有足够的小数位,以区别于周围的行(根据前面提到的Java打印规范)。

例如,如果在十进制之后只有三个位置,则无法区分0.60.6000001 (相邻位模式0x3f19999a0x3f19999b )。 因此,它可以根据需要进行打印。

你会注意到的第二件事是我们在第二个块中的0.7不是 0.7 。 相反,它是0.70000005 尽管事实上该数字的位模式更接近(在前一行)。

这是由于添加0.1导致的错误逐渐累积造成的。 您可以从最后一个块中看到,如果您只是直接使用0.7而不是连续添加0.1 ,那么您将获得正确的值。

因此,在您的特定情况下, 后一个问题会导致您遇到问题。 你打印出0.70000005的事实并不是因为Java没有得到足够接近(它有),这是因为你首先得到0.7的方式。

如果您修改上面的代码以包含:

 outLines (0.1f); outLines (0.2f); outLines (0.3f); outLines (0.4f); outLines (0.5f); outLines (0.6f); outLines (0.7f); outLines (0.8f); outLines (0.9f); 

你会发现它可以正确打印出该组中的所有数字。