为什么这是真的?

10

这是IEEE 754标准的问题。我不完全理解它背后的机制。

public class Gray {  
    public static void main(String[] args){
        System.out.println( (float) (2000000000) == (float) (2000000000 + 50));
    }
}

6
顺带一提,这个“问题”不仅限于Java;它与二进制中的浮点数表示有关,由IEEE 754标准定义。 - In silico
4个回答

29

由于 float 只能保留约7到8个有效数字。也就是说,它没有足够的比特位来精确表示数字2000000050,所以它被四舍五入为2000000000。

具体而言,float 由三部分组成:

  • 符号位(1位)
  • 指数(8位)
  • 尾数(24位,但只存储23位,因为尾数的最高有效位始终为1)

你可以把浮点数看作是计算机使用二进制进行科学计数法的方式。

精度等于log(2 ^ 尾数位数)。这意味着float可以保留log(2 ^ 24) = 7.225个有效数字。

数字2000000050有9个有效数字。上述计算告诉我们,24位尾数无法保留那么多有效数字。之所以数字2000000000可行,是因为它只有1个有效数字,所以适合放在尾数中。

要解决这个问题,可以使用double,因为它具有52位尾数,足以表示每个可能的32位数字。


我不理解这个语句的意思: 2000000000之所以有效,是因为只有1个有效数字,所以它适合于尾数。 - fabrizioM
@fabrizioM:你明白为什么数字2,000,000,000只有1个有效数字,而数字2,000,000,050却有9个有效数字吗? - In silico
好的,所以被分为2^9(2=尾数2位)(9=指数4位)。 - fabrizioM
@fabrizioM:数学实际上有些更复杂,但基本上就是这个意思。 - In silico

3
你可能会觉得这个技巧可以找到下一个可表示的值很有趣。
float f = 2000000000;
int binaryValue = Float.floatToRawIntBits(f);
int nextBinaryValue = binaryValue+1;
float nextFloat = Float.intBitsToFloat(nextBinaryValue);
System.out.printf("The next float value after %.0f is %.0f%n",  f, nextFloat);

double d = 2000000000;
long binaryValue2 = Double.doubleToRawLongBits(d);
long nextBinaryValue2 = binaryValue2+1;
double nextDouble = Double.longBitsToDouble(nextBinaryValue2);
System.out.printf("The next double value after %.7f is %.7f%n",  d, nextDouble);

打印
The next float value after 2000000000 is 2000000128
The next double value after 2000000000.0000000 is 2000000000.0000002

很好。看起来取决于尾数(请参见In silico的答案)位于int的最低有效位。我不确定在不同的架构上如何保持,但也许Java仍然能够一致地处理它? - Tony Delroy

3

简单地说,当一个浮点数的值为二十亿时,50就是一个四舍五入误差。


1
简单明了,但不准确 :-) - Stephen C
@Stephen - 你能详细说明为什么吗? - Mike Clark
1
因为50只是数字50。因为实际的舍入误差是200000000.0Real((float) 200000000)之间以及200000050.0Real((float) 200000050)之间的差异。 - Stephen C

2

如果你考虑下面这个程序(C++),可能有助于理解这种情况。它显示了一组连续整数,其舍入为相同的浮点值:

#include <iostream>                                                             
#include <iomanip>                                                              

int main()                                                                      
{                                                                               
    float prev = 0;                                                             
    int count = 0;                                                              
    double from;                                                                
    for (double to = 2000000000 - 150; count < 10; to += 1.0)                   
    {                                                                           
        float now = to;                                                         
        if (now != prev)                                                        
        {                                                                       
            if (count)                                                          
                std::cout << std::setprecision(20) << from << ".." << to - 1 << " ==> " << prev << '\n';                                                        
            prev = now;                                                         
            from = to;                                                          
            ++count;                                                            
        }                                                                       
    }                                                                           
}

输出:

1999999850..1999999935 ==> 1999999872
1999999936..2000000064 ==> 2000000000
2000000065..2000000191 ==> 2000000128
2000000192..2000000320 ==> 2000000256
2000000321..2000000447 ==> 2000000384
2000000448..2000000576 ==> 2000000512
2000000577..2000000703 ==> 2000000640
2000000704..2000000832 ==> 2000000768
2000000833..2000000959 ==> 2000000896

这意味着浮点数只能精确表示从1999999850到1999999935之间的所有整数,错误地记录它们的值为1999999872。对于其他值也是如此。这是上述有限存储空间的实际后果。


问题没有标记为C++。 - JeremyP
@JeremyP:我知道这一点,但是浮点格式通常基于硬件标准化,并且在各种语言中都很常见,因此相关概念是适用的。我只是碰巧喜欢C++而不懂Java。 - Tony Delroy
是的,但是期望Java程序员知道诸如std::cout << something这样的东西实际上是不合理的。 - JeremyP
@JeremyP:没错,但希望他们能够将程序与输出进行交叉参考,并且理解足够的内容以便在需要时用Java重写它,或者至少能够跟随高精度double如何被用来逐步遍历float无法准确表示的值的核心概念。 - Tony Delroy

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