为什么“++”有时正确,有时错误?

8
我知道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

使用double而不是float,以确保在前几个情况下的精度。 - Anirudh Ramanathan
1个回答

23
正如您所说,不是所有的数字都能在IEEE754中准确表示。结合Java用于打印这些数字的规则,这会影响您所看到的内容。
为了背景说明,我将简要介绍IEEE754的不准确性。在这种特殊情况下,0.1无法被准确表示,因此您经常会发现实际使用的数字类似于0.100000001490116119384765625。
请参见here以获取有关为什么会出现这种情况的分析。之所以会得到“不准确”的值,是因为该误差(0.000000001490116119384765625)逐渐累积。
0.10.2(或类似数字)不总是显示错误的原因与Java中的打印代码有关,而不是实际值本身。即使0.1实际上比您期望的高一点,打印它的代码也没有给出所有数字。如果将格式字符串设置为在小数点后提供50个数字,则会发现您将看到真实值。Java决定如何打印浮点数的规则(无需显式格式化)详见here。与数字计数相关的重要部分是:

必须至少有一个数字来表示小数部分,并且除此之外还需要尽可能多但仅限于唯一区分类型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.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);

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接