我知道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
) 逐渐增加。
or (0.1
或0.2
类似数字)并不总是表明错误与Java 中的打印代码有关,而不是与实际值本身有关。
尽管0.1
实际上比您预期的要高一点,但打印出来的代码并没有给您所有的数字。您会发现,如果您将格式字符串设置为传递小数点后 50 位数字,您就会看到真实值。
此处详细介绍了 Java 如何决定打印出浮点数(没有显式格式)的规则。数字计数的相关位是:
必须至少有一个数字来表示小数部分,除此之外,必须有尽可能多的数字,但仅能将参数值与浮点类型的相邻值唯一区分开来。
例如,这里有一些代码向您展示它是如何工作的:
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.6
和0.6000001
(相邻的位模式0x3f19999a
和0x3f19999b
)。因此,它会根据需要打印尽可能多的内容。
您会注意到的第二件事是我们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);
您会发现它可以正确打印出该组中的所有数字。