前言
相信大家在学习java的基本数据类型的时候都听过float和double类型都存在精度损失问题,具体什么原因很多同学都没有去进一步深究,这一篇就这个问题做一下讨论。
一、从10进制去看基本数据类型double精度丢失问题
0.012;第一个小数位为什么是0?因为0=0.012*10=0.12得不到正整数,第一个小数位能表示的最小数为1/10,最大数为9*1/10
0.012;第二个小数位为什么是1?因为1=0.012*10^2=1.2得到正整数1
0.012;第三个小数位为什么是2?因为2=(0.012-0.01)*10^3=(1.2-1)*10得到正整数2
所以:0.012=0/10+1/10^2+2/10^3
二、再来看二进制的小数表示
用double类型(64位)的二进制表示10进制数0.3:
第一位为符号位0;
11位指数位置00000000001;
52位尾数位置(10进制表示规则推导):
x/2^1+y/2^2+...+n/2^52=0.3;
也就是说,当这个数*进制得到的整数在进制可表示范围内,则记上相应的符号;
0.3*2=0.6;得不到整数,因此第1位记做0;
0.3*2^2=1.2;得到整数1,1属于2进制的计数范围内,因此第2位记做1;该位记做1了,那么数字需要减去该位表示的值,余下的再继续往后去找计数表示;
(0.3-1/2^2)*2^3=0.3*2^3-2=2(0.3*2^2-1),也就是上一轮得到的数字减去1再乘以2=0.4;所以第三位也是0;
0.4*2=0.8;----0
0.8*2=1.6;----1余0.6
...
0.3=0/2+1/2^2+0/2^3+0/2^4+1/2^5+...
所以尾数得到01001...,如果52位能够除尽不再有余数,则0.3可以用double在52位精确度范围内不丢失精度表示,反之则会丢失精度;对于10进制也是同样的道理,0.3333这个数无法尾数为3位的10进制数不丢失精度表示;1/3d对于10进制来说,无论用多少位尾数表示都无法做到精确而不丢失精度。
三、如何编码打印出double的所有二进制位
public static void printAllBinary(double d){
StringBuffer intStr = new StringBuffer("");
StringBuffer decimalStr = new StringBuffer("");
int intPart = (int)d;
double decimalPart = d%1;
//整数转2进制方法:不断除以2取余数,最后将余数倒序
while(intPart>0){
intStr.append(intPart%2);
intPart/=2;
}
System.out.println(d+"整数部分二进制:"+intStr.reverse().toString());
//小数转2进制方法:不断乘以2判断是否>=1,如果>=1则该位置1否则该位置0
int maxBit=1;
while(decimalPart!=0 && maxBit++<60){
decimalPart*=2;
if(decimalPart>=1){
decimalPart-=1;
decimalStr.append("1");
}else{
decimalStr.append("0");
}
}
System.out.println(d+"小数部分二进制:"+decimalStr.toString());
// //double为双精度浮点数:1符号位+11指数位+52尾数位
// StringBuffer doubleStr = new StringBuffer("");
// //符号位
// String sign = "0";
// if(intPart<0){
// sign = "1";
// }
// //指数位置
// int offset = 1023;//double偏移量
// intStr.length()-1+
// intStr.append(decimalStr)
}
四、那么问题来了,在尾数数量有限的情况下,数需要满足什么规律才能被精确展示?
先看10进制,假设尾数最多5位,那么0.99999是精确的,因为0.99999=9*1/10+9*1/100+9*1/1000+...+9*(1/10^5),即如若x=(进制内数字)*(1/10^1)+(进制内数字)*(1/10^2)+...+(进制内数字)*(1/10^n)成立(其中1=<n<=5),则x可以被五位小数有效位的10进制精确表示;
同样的道理,如果想要double能够精确表示某个小数x,那么x=(0或者1)*1/2^1+(0或者1)*1/2^2+(0或者1)*1/2^3+...+(0或者1)*1/2^n(其中1=<n<=52)需要成立;举个例子,0.25是可以被double精确表示的,因为0.25=1*(1/2^2),二进制表示为:0000000010100000..到64位;
五、double的数据范围?
1、机器数基础补充
机器数是有符号位的二进制数,规定用它的最高位表示符号位,0表示正数,1表示负数。
为了使机器数的运算方法适用于所有机器数("+"、"-"等运算),同时保证运算不会出现二义性,在机器数的运算方法演变过程中出现了三种不同的机器数编码方式:原码、反码、补码、阶码、移码;
1.1原码
原码是一个数的带符号位的二进制表示。以10进制数5举例:
正数原码:0101
负数原码:1101
1.2反码
二进制位全部取反,称为原二进制数的反码;
1.3补码
以10进制数5举例:
正数补码:同原码一致:0101
负数补码:原码取反+1(符号位不参与):1011
机器数都是通过补码来进行运算;
1.4阶码
阶码是机器数里浮点数中的称谓,阶码指的就是浮点数中的指数,它表示了浮点数中这个小数点的具体位置。
阶码通常是原码形式。
1.5移码
又称增码。移码有两个很重要的作用:
1、方便机器数比较大小。
2、用于浮点数中"阶码"的修正(实际上就是为了将浮点数的表现形式都规范化)。
大小比较:
比如用6位二进制位表示十进制数31:
31的表示是:011111
-31的表示是:100001
看起来好像100001>011111,实际相反,所以虽然补码方便计算,但对于大小比较不直观。于是移码通过在原补码基础上+2^n(n表示原码真值部分最大位数,这里是5)
31补码+2^5:111111
-31补码+2^5:000001
-30补码+2^5:000010
从上面就能很直观的看出:31>-30>-31。
阶码修正:
阶码就是指数,指数也是有正负的。阶码采用移码来表示好处是可以用长度为n个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易。
2、科学计数法
10进制科学计数法定义:
3、float指数取值范围为什么是-126~128?
根据IEEE754国际标准,移码就是真值加2^n构成的,8位移码表达0~255,所以表达的真值就是-128~127(真值+128变成对应的移码)
我们再来看一下float这个浮点数,23位尾数是原码表达,用来表达小数点后23位。那我们来考虑,浮点数怎么取到0,刚好0而不是近似0。注意,最小正值只能到2^(-126),因为就算尾数全取0,前面还有一个隐藏位的1在啊,就算阶数取到最小,也是近似0而不是真正的0。所以IEEE那帮人就想了,那直接规定让阶数等于0的时候,隐藏位的1也变成0算了。
所以阶数全0,尾数全0,实际上是在表达0点。所以回到移码对应的真值数,我们发现-128表示不出来了,现在阶码的真值范围变成了-127~127。所以偏移量最大只能到达127,而不是128。
那么阶码127,真值为(127-127=0)的时候表示的是什么?当然就是1.m(尾数自己本身)。
很遗憾告诉你们一件事,刚刚的-127~127只是在移码的理想情况下证明为什么偏移量只能取127。实际上阶码的真值,也就是2的指数取值范围还要更小--只有(-126~127),为什么呢?因为还有一个数(阶数全1)被用作浮点数表达正负无穷大和NaN了。阶数全1,尾数全0表达无穷大(正负性看数符),阶数全1,尾数不为0,是NaN。
4、范围计算
最大值计算:
符号位:0
指数位:11111111111:2^11-1=
六、double直接打印0.1为什么显示没有问题?
七、不可精确表示浮点数经过运算为什么有的结果正确,有的不正确?
public static void main(String[] args) {
double d1 = 2.1;
double d2 = 2.3;
double d3 = d2 - d1;
double d4 = d2 + d1;
System.out.println("d2-d1:"+d3);
System.out.println("d2+d1:"+d4);
}
执行结果如下:
分析:运算正确属于巧合?
总结
解决java基本类型中浮点类型的精度丢失问题,可以通过java对象bigdecimal的字符串构造方法来解决。因为bigdecimal是一个不变对象,通过转化为整数来进行运算。