为什么 `2.0 - 1.1` 和 `2.0F - 1.1F` 会产生不同的结果?
Why `2.0 - 1.1` and `2.0F - 1.1F` produce different results?
我正在编写比较 Double
和 float
值的代码:
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.1
但 2.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.
float
和double
的区别:
- IEEE 754 single-precision binary floating-point format
- IEEE 754 double-precision binary floating-point format
让我们在一个简单的 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 位之间的小数。
顺便问个好问题...
我正在编写比较 Double
和 float
值的代码:
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.1
但 2.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.
float
和double
的区别:
- IEEE 754 single-precision binary floating-point format
- IEEE 754 double-precision binary floating-point format
让我们在一个简单的 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 位之间的小数。
顺便问个好问题...