本文不仅仅适用于java
问题
先上两段代码,经过本文的讲解后,你应该能深刻的理解浮点数并且可能不想用"浮点数"(float/double)了
问题1
float a = 0.1f;
float b = 0.2f;
float c = a +b;
System.out.println(Float.compare(0.3f,a+b));
System.out.println(c-0.3f);
System.out.println("===================");
double a1 = 0.1;
double b1 = 0.2;
double c1 =a1 +b1;
System.out.println(Double.compare(0.3d,c1));
System.out.println(c1 -0.3);
System.out.println(Math.pow(2,-54));
输出
true
0.0
===================
false
5.551115123125783E-17
5.551115123125783E-17
为什么float 的0.3相等,而double的0.3不想等,相差了Math.pow(2,-54)
问题2
float x = 0.0f/0.0f;
System.out.println(x);
输出
NaN
为什么输出的是NaN,而不是报java.lang.ArithmeticException: / by zero
这个异常
IEEE 754
我们知道任何数据在内存中都是以二进制存储的,所以IEEE 754 规定了浮点数在计算机中二进制的存储方式,java语言也遵守这一标准(哪个流行语言敢不遵守)。
浮点数类型
单/双精度浮点数分别对应java语言中的float和double
单精度浮点数
双精度浮点数
可以从上面的图可以看出,在 IEEE 754标准下,浮点数的二进制格式分为三部分,类似于科学计数法。
S代表符号位,0代表正,1代表负
E代表指数,b代表指数有多少位
M为尾数,n代表尾数有多少位
二进制数转换为浮点数
从二进制数转换为浮点数的公式如下
上面的公式有2个点,大家可能不理解,但是它的设计巧妙十分巧妙
第一,为什么E需要减去一个掩码的大小?因为E生成的时候加上了一个掩码的大小。
那么,为什么要加上一个掩码的大小呢?
首先我们的指数是有正负的,所以E的最高位为符号位,以8位的E为例子,使用使用普通的符号数,它能表示数字范围为 +0 ~ 127,-0~ -127,而对于一个数来当指数来讲,我们不需要2个0。所以通过加上一个掩码的大小(专业术语叫做移位存储)来解决这个问题。通过移位存储,我们能表达的数字范围为0 ~ 128,0~ -127 。
移位存储你也可以理解为用无符号数来映射有符号数
第二,为什么M前需要加上1?因为尾数M省略了一位。经过移位后,第一位强制为1,所以省略了。n位的尾数实际上是n+1位。
第一位强制为1?那不是不可能代表0了,浮点数的0怎么表示?这个下面再讲。
浮点数转换为二进制数
整数部分,除以2,不断取余数的1和0,结果倒序排列
小数部分,乘以2,不断取整数的1和0,结果正序排列
以10.5,10.2 ,0.2 和 0.5举例子
对于整数部分的10来讲,我们可以得到它的二进制为1010
对于0.5来讲
0.5 * 2 = 1.0 取1
所以0.5的二进制为.1
对于0.2来讲
0.2 * 2 = 0.4 取0
0.4 * 2 = 0.8 取0
0.8 * 2 = 1.6 取1
0.6 * 2 = 1.2 取1
0.2 * 2 = 0.4 取0
循环下去
所以0.2的二进制为 .001 1001 1001 1001...
规范起见,我们使用到的浮点数二进制需要通过幂次进位保证整形部分必须为1
以32位的单精度浮点数举例
10.5 的二进制表示为 0 10000010 0101 0000 0000 0000 0000 000
10.2 的二进制表示为 0 10000010 0100 0110 0110 0110 0110 011
0.5的二进制表示为 0 01111110 0000 0000 0000 0000 0000 000
0.2的二进制表示为 0 01111100 1001 1001 1001 1001 1001 101
转换代码
人肉转换太痛苦了,上代码
public static void main(String[] args) {
getFloatBin(0.1f);
getFloatBin(0.2f);
getFloatBin(0.3f);
getDoubleBin(0.1d);
getDoubleBin(0.2d);
getDoubleBin(0.3d);
}
public static void getFloatBin(float f){
int b=Float.floatToIntBits(f);
String s = Integer.toBinaryString(b);
int size = s.length();
for(int i =0 ;i < 32-size;i++){
s = "0" +s;
}
for(int i =0 ;i< s.length();i++){
if(i ==1 || i ==9){
System.out.print(",");
}
if(i >8 && (i-9)%4 ==0){
System.out.print(" ");
}
System.out.print(s.charAt(i));
}
System.out.println();
}
public static void getDoubleBin(double d){
long l = Double.doubleToLongBits(d);
String s = Long.toBinaryString(l);
int size = s.length();
for(int i =0 ;i < 64-size;i++){
s = "0" +s;
}
for(int i =0 ;i< s.length();i++){
if(i ==1 || i ==12){
System.out.print(",");
}
if(i >12 && (i-12)%4 ==0){
System.out.print(" ");
}
System.out.print(s.charAt(i));
}
System.out.println();
}
可以看出一个规律,只要不是5结尾的浮点数,对应的二进制都是无限循环,必须做舍入,二进制的舍入很简单,只要下一位是1,就进位
特殊数值处理
上面讲到指数的E的取值范围是0 ~ 128,0~ -127,但是 128(11111111) 和 -127(00000000) 这两个指数对应的浮点数有特殊的含义。
+0
-0
+∞
-∞
NAN
NAN = not a number ,浮点数的除以0操作会返回这个
非规范化数
除了+/-0以外,E=00000000的浮点数用于表示一些很小,很接近于0的数字,由于这个数省略了0开头所以叫非规范化数,对应省略1开头的叫规范化数。知道有这个存在即可,基本用不上。
浮点数的范围
知道了浮点数的二进制格式,那么它的范围也是可以轻松推出来的
规范化-最小值-绝对值
00 80 00 00 = 2-126 * (1+0/223)= 2-126 ≈ 1,17549435∙e-38
80 80 00 00 = -2-126 * (1+0/223)=-2-126≈ -1,17549435∙e-38
规范化-最大值-绝对值
7F 7F FF FF = 2127(2-2-23) = 2128≈ 3,40282347∙e+38
FF 7F FF FF = -2127(2-2-23) = -2128≈ -3,40282347∙e+38
非规范化浮点数就不列了,自行研究
解答开篇问题
问题1
对于单精度浮点数,也就是float,0.1+0.2计算逻辑如下
0.1 = 0,01111100,1001 1001 1001 1001 1001 101
= 1,1001 1001 1001 1001 1001 101 * 2^-4
0.2 = 0,01111100,1001 1001 1001 1001 1001 101
= 1,1001 1001 1001 1001 1001 101 * 2^-3
0.1 + 0.2 =
1,1001 1001 1001 1001 1001 101 * 2^-4 +
11,0011 0011 0011 0011 0011 01 * 2^-4
= 100,1100 1100 1100 1100 1100 111 * 2^-4
= 1,0011 0011 0011 0011 0011 001(1) * 2^-2
= 1,0011 0011 0011 0011 0011 010 * 2^-2
0.3 = 0,01111101,0011 0011 0011 0011 0011 010
= 1, 0011 0011 0011 0011 0011 010 * 2^-2
对于双精度浮点数,也就是double类型,计算逻辑如下
0.1 = 0,01111111011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
= 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4
0.2 = 0,01111111100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
= 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -3
0.1 + 0.2 =
1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4 +
11,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010 * 2 ^ -4
= 100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1110 1100 1100 1110 * 2 ^ -4
= 1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (10) * 2 ^ -2
= 1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 * 2 ^ -2
0.3 = 0,01111111101,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
= 1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 * 2 ^ -2
浮点数转换为二进制后,存在无限循环的问题,因此到达有效位后会进行舍入,对于二进制数是1进位0不进位的规则。
在32位浮点数的 0.2 +0.1中,0.1,0.2,0.1+0.2,0.3,都是存在舍入操作的并且进位一样多。
而在64位浮点数的0.2+0.1中,由于有效数字是52位,刚好是4的倍数,并且后面接的二进制0011不会产生进位,但是在0.1 + 0.2 的过程中由于指数不对齐产生了进位,才导致0.1+0.2大于了0.3。
问题2
java语言的浮点数实现是遵循了IEEE 754规范,而NAN也是这个规范的要求之一。所以对于浮点数的除以0操作是会返回NAN的。
如何在java程序中处理浮点数
经过上面的分析,显而易见,如果你在java应用中使用float 或者 double类型进行运算,肯定是会存在精度问题的。
所以在应用中关于金额的字段务必使用BigDecimal或者使用整型进行操作!!!
如果不进行运算操作,还是可以照常使用的
至于BigDecimal的原理就是,整型在转化为二进制的时候不会存在精度问题。
参考
IEEE 754 标准
IEEE 754浮点数标准详解
为什么说浮点数缺乏精确性? python中浮点数运算问题
浮点数在内存中的表示移位存储难点的理解
网友评论