Java 中 6.6f + 1.3f != 7.9f ? 到底是什么鬼?

/ Java / 没有评论 / 1340浏览

Java 中 6.6f + 1.3f != 7.9f ? 到底是什么鬼?

今天早上有网友在群里说感觉他自己什么都会,我感觉他膨胀了,就给他出了一个基础题。把他难坏了,让我给他解释为什么?下面我们就一起来讨论讨论这个问题。

float f1 = 6.6f;
float f2 = 1.3f;
System.out.println(f1 + f2);

打印结果是:7.8999996。什么个鬼,我的程序难道是个假程序吗?

我们将 float 改为 double,在执行一下。

double f1 = 6.6f;
double f2 = 1.3f;
System.out.println(f1 + f2);

结果又变了,为:7.8999998569488525。

这是为什么呢?为什么和我预期的不一样。

要说明这个问题,我们就要从计算机的底层的 0 和 1 说起。计算机只认识 0 和 1,所以所有的计算最终都会转换成二进制的计算。

float 存储原理

CPU 表示浮点数由三部分组成 分为三个部分,符号位(sign),指数部分(exponent)和有效部分(fraction, mantissa)。 其中 float 总共占用 32 位,符号位,指数部分,有效部分各占 1 位,8 位,23 位。

CPU 表示浮点数的组成

对于实数,转化为二进制分为两部分,第一部分整数部分,第二部分是小数部分。整数部分计算二进制大家都很熟悉。

6 转换为二进制的过程

我们再看一个小数部分的计算过程。将小数乘以2,取整数部分作为二进制的值,然后再将小数乘以2,再取整数部分,以此往复循环。

0.6 转化为二进制的过程

你会发现,上面的计算过程会发生循环,循环体为 1001。所以 0.6 转化为二进制为 0.10011001…,6.6转化为二进制为 110.10011001… 无限循环。

那么计算机该如何处理小数呢?人们是非常聪明的,所以想出了“规约化”和“指数偏移值”。

规约化

规约化,就是我们通过规约化将小数转为规约形式,类似我们用的科学计数法,就是保证小数点前面有一个有效数字。

在二进制里面,就是保证整数位是一个 1。那么 110.10011001 规约化后就为:1.1010011001*2^2

指数偏移值

是指浮点数中指数部分的值,它的值为规约形式的指数值加上某个固定的值,float 的固定值为 127,计算方法是 2^e-1 其中的 e 为存储指数部分的比特位数,前面提到的 float 为 8 位,double 为 11 位。在这里,因为是 2 的 2 次方,偏移值就是 127+2=129,转换为二进制就是 10000001

拼接

前面说了,采用二进制科学计数法计算浮点数的,有三个部分。符号位,指数部分,有效部。

6.6 为正数,符号位为 0,指数部分为偏移值的二进制 10000001,有效部分为规约形式的小数部分,为什么只取小数部分?因为整数肯定是 1,去掉了不会产生误差。我们去取小数的前 23 位即 10100110011001100110011,最后拼接到一起即 01000000110100110011001100110011。 同理,我们可以计算出 1.3 的浮点数为 00111111101001100110011001100110。 浮点计算精度损失原因

至此完成 6.6 + 1.3的过程,加减乘除方法类似,有兴趣自行搜索。

参考资料