为什么 `2.0 - 1.1` 和 `2.0F - 1.1F` 会产生不同的结果?

Why `2.0 - 1.1` and `2.0F - 1.1F` produce different results?

我正在编写比较 Doublefloat 值的代码:

class Demo {
    public static void main(String[] args) {
        System.out.println(2.0 - 1.1);            // 0.8999999999999999
        System.out.println(2.0 - 1.1 == 0.9);     // false

        System.out.println(2.0F - 1.1F);          // 0.9
        System.out.println(2.0F - 1.1F == 0.9F);  // true
        System.out.println(2.0F - 1.1F == 0.9);   // false
    }
}

输出如下:

0.8999999999999999
false
0.9
true
false

我相信 Double 值比 float 更能节省精度。

请解释一下,貌似float值不是丢精度而是double丢精度?


编辑: @goodvibration 我知道 0.9 不能用任何计算机语言准确保存,我只是很困惑 java 如何详细地处理它,为什么 2.0F - 1.1F == 0.9F,但是 2.0 - 1.1 != 0.9,另一个有趣的发现可能会有所帮助:

class Demo {
    public static void main(String[] args) {
        System.out.println(2.0 - 0.9);            // 1.1
        System.out.println(2.0 - 0.9 == 1.1);     // true

        System.out.println(2.0F - 0.9F);          // 1.1
        System.out.println(2.0F - 0.9F == 1.1F);  // true
        System.out.println(2.0F - 0.9F == 1.1);   // false
    }
}

我知道我不能指望浮点数或双精度,只是..想不出来让我发疯,这背后的真正意义是什么?为什么 2.0 - 0.9 == 1.12.0 - 1.1 != 0.9 ??

突击测试:代表1/3,十进制。

答:不能;不准确。

计算机以二进制计数。 'cannot be completely represented' 还有更多的数字。就像在小数题中,如果你只有一张小纸条可以写,你可以简单地用 0.3333333 收工,然后你就会得到一个非常接近的数字1 / 3,但不完全相同,计算机也代表分数。

或者,这样想:一个float占32位; a double 占64。一个32位的值只能表示2^32(约40亿)个不同的数字。然而,即使在 0 和 1 之间也有无数个数字。因此,鉴于最多有 2^32 个特定的具体数字 'representable precisely' 作为浮点数,任何不在大约 40 亿个值的祝福集中的数字都是不可表示的。而不是仅仅出错,您只需在这个 40 亿个值的池中得到一个可以表示的值,并且是最接近您想要的值的一个。

此外,由于计算机以二进制而不是十进制计数,因此您对什么是 'representable' 什么不是的感觉是错误的。你可能认为 1/3 是个大问题,但 1/10 肯定很容易,对吧?那只是 0.1,这是一个精确的表示。啊,但是,十分之一在十进制中很好用。毕竟,十进制是以数字 10 为基础的,这并不奇怪。但是二进制呢?二分之一、四分之一、八分之一、十六分之一:用二进制表示很简单。十分之一?这和第三个一样困难:不可代表

0.9 本身不是一个可表示的数字。然而,当您打印浮点数时,这就是您得到的结果。

原因是,印刷 floats/doubles 是一门艺术,而不是一门科学。鉴于只有少数数字是可表示的,并且由于二进制与十进制的关系,这些数字对人类来说感觉不到 'natural',因此您确实需要向数字添加 'rounding' 策略或它看起来很疯狂(没人想读 0.89999999999999999765)。而这正是 System.out.println 和合作伙伴所做的。

但您确实应该控制舍入函数:切勿使用 System.out.println 打印双精度数和浮点数。请改用 System.out.printf("%.6f", yourDouble);,在这种情况下,两者都会打印 0.9。因为虽然实际上两者都不能精确地表示 0.9,但在浮点数中最接近它的数字(或者更确切地说,当你取最接近 2.0 的数字(即 2.0)和最接近 1.1 的数字(不是1.1), 减去它们,然后找到最接近该结果的数字) – 打印为 0.9,即使它不是浮点数,也不会打印为 0.9 in double.

floatdouble的区别:

让我们在一个简单的 C 程序中 运行 你的数字,以获得它们的二进制表示:

#include <stdio.h>

typedef union {
    float val;
    struct {
        unsigned int fraction : 23;
        unsigned int exponent :  8;
        unsigned int sign     :  1;
    } bits;
} F;

typedef union {
    double val;
    struct {
        unsigned long long fraction : 52;
        unsigned long long exponent : 11;
        unsigned long long sign     :  1;
    } bits;
} D;

int main() {
    F f = {(float )(2.0 - 1.1)};
    D d = {(double)(2.0 - 1.1)};
    printf("%d %d %d\n"      , f.bits.sign, f.bits.exponent, f.bits.fraction);
    printf("%lld %lld %lld\n", d.bits.sign, d.bits.exponent, d.bits.fraction);
    return 0;
}

这段代码的打印输出是:

0 126 6710886
0 1022 3602879701896396

根据以上两种格式规范,我们将这些数字转换为有理数。

为了达到高精度,让我们用一个简单的Python程序来实现:

from decimal import Decimal
from decimal import getcontext

getcontext().prec = 100

TWO = Decimal(2)

def convert(sign, exponent, fraction, e_len, f_len):
    return (-1) ** sign * TWO ** (exponent - 2 ** (e_len - 1) + 1) * (1 + fraction / TWO ** f_len)

def toFloat(sign, exponent, fraction):
    return convert(sign, exponent, fraction, 8, 23)

def toDouble(sign, exponent, fraction):
    return convert(sign, exponent, fraction, 11, 52)

f = toFloat(0, 126, 6710886)
d = toDouble(0, 1022, 3602879701896396)

print('{:.40f}'.format(f))
print('{:.40f}'.format(d))

这段代码的打印输出是:

0.8999999761581420898437500000000000000000
0.8999999999999999111821580299874767661094

如果我们在指定 8 到 15 位十进制数字的同时打印这两个值,那么我们将遇到与您观察到的相同的事情(double 值打印为 0.9,而 float打印值接近 0.9):

换句话说,这段代码:

for n in range(8, 15 + 1):
    string = '{:.' + str(n) + 'f}';
    print(string.format(f))
    print(string.format(d))

给出这个打印输出:

0.89999998
0.90000000
0.899999976
0.900000000
0.8999999762
0.9000000000
0.89999997616
0.90000000000
0.899999976158
0.900000000000
0.8999999761581
0.9000000000000
0.89999997615814
0.90000000000000
0.899999976158142
0.900000000000000

因此,我们的结论是 Java 默认打印精度在 8 到 15 位之间的小数。

顺便问个好问题...