有关Java浮点数精度丢失的一个有趣的例子以及尚未解决的疑惑
本文章重点在于表述最后的例子和尚未解决的疑问,故对于浮点数相关概念只是简单进行叙述
一、浮点数的存储模式
Java 语言支持两种基本的浮点类型: float 和 double 。java 的浮点类型都依据 IEEE 754 标准。IEEE 754 定义了32 位和 64 位双精度两种浮点二进制小数标准。
IEEE 754 用科学记数法以底数为 2 的小数来表示浮点数。
对于32 位浮点数float用 第1 位表示数字的符号,用第2至9位来表示指数,用 最后23 位来表示尾数,即小数部分。
- float(32位):
对于64 位双精度浮点数,用 第1 位表示数字的符号,用 11 位表示指数,52 位表示尾数。
- double(64位):
(1)一个单独的符号位s 直接编码符号s 。
(2)k 位的幂指数E ,移码表示 。
(3)n 位的小数,原码表示 。
底数部分 使用2进制数来表示此浮点数的实际值。
指数部分 占用8-bit的二进制数,可表示数值范围为0-255。 但是指数应可正可负,所以IEEE规定,此处算出的次方须减去127才是真正的指数。所以float的指数可从 -126到128.
底数部分实际是占用24-bit的一个值,由于其最高位始终为 1 ,所以最高位省去不存储,在存储中只有23-bit。
-
规格化(标准化)与移位
(以float为例进行阐述)
规格化的原因:在存储时,需要进行规格化操作,使用尾数来存储数值信息,而指数部分只记录偏移量,所以进行规格化之后能够统一存储格式 -
移位原因:
根据IEEE 754标准,需要在指数部分+127
指数有正有负,+127之后统一存储为正数,节省一位有效位
二、什么时候出现无法表示?
任何一个浮点数字,在底层表示都必须转换成这种科学计数法来表示,那么我们来想想看什么时候这个数字会无法表示呢?那么只有两种情形:
-
幂数不够表示了:这种情况往往出现在数字太大了,超过幂数所能承受的范围,那么这个数字就无法表示了。如幂数最大只能是10,但是这个数字用科学计数法表示时,幂数一定会超过10,就没办法了。
-
尾数不够表示了:这种情况往往出现在数字精度太长了,如1.3434343233332这样的数字,虽然很小,还不超过2,这种情况下幂数完全满足要求,但是尾数已经不能表示出来了这么长的精度。
三、表示范围及有效位
- float的范围为-2^128 ~ +2^127,也即-3.40E+38 ~ +3.40E+38
- double的范围为-2^1024 ~ +2^1023,也即-1.79E+308 ~ +1.79E+308
四、例子及疑惑
在上课的时候,随意瞎敲了一个例子,根据二进制转换想要理解浮点数的存储过程,计算得到的32bit的结果进行比对,发现有一位是不同的,原以为输出该是false,可输出的结果却是true。这是在是让人不解
例子
float d = 10.0000005f;
float f = 10.000001f;
System.out.println("(d=10.0000005) == (f=10.000001) ? "+(d == f));
运行结果为:true
可能第一反应就是 丢精度了呗,最后精度丢了呗,有啥稀奇的。当时我也是这么想的,接下来我们手动进行一下二进制存储的转换
d = 10.0000005
直接准换为二进制:
1010.0000000000000000000010000110001101111011110100001
进行规格化,移动三位 E11
1.0100000000000000000000010000110001101111011110100001 E11
根据IEEE 754标准 指数+127,为
00000011
+01111111
———————————
10000010
加上符号位 0
存储内容应为(尾数取后23位):
0 10000010 01000000000000000000000 《--10.0000005f
f = 10.000001
直接准换为二进制:
1010.0000000000000000000100001100011011110111101000001
进行规格化,移动三位 E11
1.0100000000000000000000100001100011011110111101000001 E11
根据IEEE 754标准 指数+127,为
00000011
+01111111
———————————
10000010
加上符号位 0
存储内容应为(尾数取后23位):
0 10000010 01000000000000000000001 《--10.000001f
然后进行对比:
0 10000010 01000000000000000000000 《--10.0000005f
0 10000010 01000000000000000000001 《--10.000001f
?????问题来了,这tm,判断结果是true?嗯????
哈哈哈,不对啊,32bit最后一位存进去,一个是0,一个是1,结果是 true???
ei?不太对啊这个。
然后直接转成二进制来一把:
public static void main(String[] args){
float d = 10.0000005f;
float f = 10.000001f;
//System.out.println("(d=10.0000005) == (f=10.000001) ? "+(d == f));
int k = Float.floatToIntBits(f);
System.out.println(Integer.toBinaryString(k));
k = Float.floatToIntBits(d);
System.out.println(Integer.toBinaryString(k));
}
结果:
emmmm,这几个意思???
没招了,再debug一把?
阿嘞?? debug出来d的值就是10.000001,f的值还是10.000001。
难道是在存储的时候被四舍五入掉了?所以才会返回true吗??可是这个进的一位是从哪来的?jvm是怎么进行的取舍呢?这里面究竟是怎么个一个情况呢???
如果有大佬看到了这里,并且了解相关的内容,一定要来帮我解解惑啊~~~~