计算机如何表示浮点数?
从一个最简单也最经典问题说起:
能说一说
System.out.println( 1f == 0.999999999999f );
的打印结果是什么吗?这么写有什么问题吗?
对于这样一个问题,回答结果一般也就两种情况。
其实这个题目考察的目的简单而明确:浮点数在计算机中是如何运算的?写代码时有什么要注意的?会有哪些坑?能说出这3个方面基本就可以了,但有些小伙伴可能忘记了。
那有同学会说了,考这样一个破题目有实际意义吗?工作中能遇到这种情况???
你别说,以前代码走查时还真看到过这种用==
来进行浮点数等值判断的代码,而且这种浮点数的精度问题在工作中还是有相当的概率会遇到的,一旦没有发现,上线后往往就会出大问题,要背锅的。。。
连《阿里巴巴Java开发手册》中都有一条强制性规约和浮点数运算有关,所以其重视程度可见一斑:
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
”
一些奇怪的现象
在涉及诸如float
或者double
这类浮点型数据处理时,偶尔总会有一些奇葩问题,从而会导致各种超出预期的现象发生,以前也举过例子,比如:
- 条件判断超预期
System.out.println( 1f == 0.9999999f ); // 打印:false
System.out.println( 1f == 0.99999999f ); // 打印:true ?
- 数据转换超预期
float f = 1.1f;
double d = (double) f;
System.out.println(f); // 打印:1.1
System.out.println(d); // 打印:1.100000023841858 ?
- 基本运算超预期
System.out.println( 0.2 + 0.7 );
// 打印:0.8999999999999999 ?
- 数据自增超预期
float f2 = 84552631f;
for (int i = 0; i < 10; i++) {
System.out.println(f2);
f2++;
}
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
// 打印:8.4552632E7 ?
所以如果在业务代码中一旦涉及到诸如:订单金额、商品金额、交易值、货币运算等这种对精度要求很高的场景时,使用浮点数就一定要慎重了,一不注意就可能锅从天而降了,而且排查起来有时候还挺费劲。
计算机是怎么表示小数的?
学过 《计算机组成原理》 或者类似 《计算机系统》 这些课程的小伙伴们应该都知道,浮点数在计算机中的存储方式遵循IEEE 754 浮点数计数标准,可以表示为:
采用尾数 + 阶码的编码方式,更通俗一点说,就是类似于数学课本上所学的科学计数法表示方式:有效数字 + 指数位!
因此,只要给出:符号(S)、阶码部分(E)、尾数部分(M) 这三个维度的信息,一个浮点数的表示就完全确定下来了,所以float
和double
这两种类型的浮点数在计算机中的存储结构就表示成下图所示这个样子:
1、符号部分(S)
0
-正 1
-负
2、阶码部分(E)(指数部分):
- 对于
float
型浮点数,指数部分8
位,考虑可正可负,因此可以表示的指数范围为-127 ~ 128
- 对于
double
型浮点数,指数部分11
位,考虑可正可负,因此可以表示的指数范围为-1023 ~ 1024
3、尾数部分(M):
浮点数的精度是由尾数的位数来决定的:
- 对于
float
型浮点数,尾数部分23
位,换算成十进制就是2^23=8388608
,所以十进制精度只有6 ~ 7
位; - 对于
double
型浮点数,尾数部分52
位,换算成十进制就是2^52 = 4503599627370496
,所以十进制精度只有15 ~ 16
位
所以,浮点数交给计算机存储的时候,可能会有精度丢失问题!!!因此使用时需要格外小心,如果真因为这一块出了bug,定位问题还是非常艰难的,所以预防工作要做好。
小数怎么换算成二进制?
上面说的是IEEE标准规定的内容,属于理论规约。那一个小数到底要怎么换算成二进制呢?我们得拿实际例子来解释。
先来个简单的例子
比如:把十进制小数0.875
转换成二进制,具体怎么操作?
可以分几大步走:
1、以小数点为界,拆分
2、整数部分转换
整数转二进制我想大家应该都熟悉,使用:除2取余法 即可。而这里的0.875
整数部分为0,无需操作。
3、小数部分转换
小数部分的转换不同于整数部分,采用的是 “乘2取整法” ,图示一下就明白了:
4、合并结果
整数部分 + 小数部分
,最终得到二进制结果为0.111
。
所以该结果按照上一节所述的尾数 + 阶码的计算机计数方式,则可以表示为:
所以对应可得:
- 符号位:
0
- 阶码(E)部分:若以
float
为例,应为127 +(-1)= 126
,因此二进制表示为:01111110
- 尾数部分(M):若以
float
为例,应为23
位,因此尾部补齐后为11000000000000000000000
。
因此最终的总结果为(以32
位精度float
表示):
00111111011000000000000000000000
再来个复杂点例子
再比如:把十进制小数6.36
转换成二进制,具体怎么操作?
但凡能用图示,我就不想写文字,所以用一张图就可以解释得明明白白:
整数部分 + 小数部分,因此最终得到的结果二进制结果为110.01011100...
。
还是按照上一节所述的尾数 + 阶码的计算机计数方式,则可以表示为:
所以对应可得:
- 符号位:0
- 阶码(E)部分:若以
float
为例,应为127 +(2)= 129
,因此二进制表示为:10000001
- 尾数部分(M):
1001011100...
,但若以float
型精度来截取23
位,则可以表示为10010111000010100011111
因此最终的总结果为(以32
位精度float
表示):
01000000110010111000010100011111
因此像这种无限位数的尾数情况,用计算机存储产生截取是必然的,必定会有一定的精度损失!这也从根本上解释了为什么float
或者double
这种类型数据使用时的风险性,因此必须要结合实际业务理性考量。
所以回到文章开头的那个问题:System.out.println( 1f == 0.999999999999f );
,换算一下你就会发现,其实在float
类型下,不管是1f
还是0.999999999999f
,它们的二进制换算结果都是:
00111111 10000000 00000000 00000000
所以结果也就不奇怪了。
binaryconvert
大家如果对上面的计算结果不放心,或者想检查手动换算的结果是否正确,也有直接的这种二进制转换工具站,典型的比如binaryconvert
。
不想手动换算的,直接去上面输入,转换一下即可得到结果,而且可以进制互换,使用非常方便。
小 结
所以情况大致就是这样,总之业务代码中一旦涉及到浮点数,就得提高警惕,格外小心一些!