当前位置: 首页 > 面试题库 >

为什么加倍有时是对的,有时是错的?

司空福
2023-03-14
问题内容

我知道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打印规范)。

例如,如果小数点后仅3个位,则您将无法区分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);

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



 类似资料:
  • double.java的源代码和一些常量类似 但我想知道为什么它不抛出AlithmeticException(divide by zero)? 我已经试过了 它没有抛出异常... 有什么魔力,为什么它不抛出异常?

  • 我需要在我的中添加一个新的目录位置,但问题是我使用的是一个全新安装的系统(Linux),其中尚未定义任何。我读过并使用过,我认为我很了解它,但我不知道当没有存在时会发生什么。 我不能附加到不存在的东西上,但我希望当前发现的所有重要库都能正常工作,因此要小心,我在Python中使用了来获取所有标准值。然后我为定义了一个-变量,包括我刚刚找到的所有节点,以及我的新目录。但是哇,很多东西都停止工作了!P

  • 问题内容: 什么是反射,为什么有用? 我对Java特别感兴趣,但是我认为原理在任何语言中都是相同的。 问题答案: 名称反射用于描述能够检查同一系统(或本身)中的其他代码的代码。 例如,假设您在Java中有一个未知类型的对象,并且想在该对象上调用“ doSomething”方法(如果存在)。除非对象符合已知的接口,否则Java的静态类型化系统并不是真正为支持该类型而设计的,但是使用反射,您的代码可以

  • 本文向大家介绍GC是什么? 为什么要有GC?相关面试题,主要包含被问及GC是什么? 为什么要有GC?时的应答技巧和注意事项,需要的朋友参考一下 答 GC(Garbage Collection) GC是垃圾收集器。程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一: 当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有无被引用的对象,进

  • 本文向大家介绍对于有压力时,你是怎么抗压的?相关面试题,主要包含被问及对于有压力时,你是怎么抗压的?时的应答技巧和注意事项,需要的朋友参考一下 &nbsp;好的 … ------------------&nbsp;原始邮件&nbsp;------------------ 发件人: "itsmyturn"<notifications@github.com&gt;; 发送时间: 2020年6月23日(

  • 首先,澄清一下,我说的不是取消引用无效指针! 考虑以下两个例子。 例1 [3.17]不确定值-未指定值或陷阱表示 后来呢: [6.2.4p2]当指针指向的对象达到其生存期的末尾时,指针的值就变得不确定。 所有这些加在一起,我们在访问指向“死”对象的指针时有什么限制?