java中double类型为什么会丢失精度?(从我们熟悉的10进制去看计算机的2进制精度)

前言

相信大家在学习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是一个不变对象,通过转化为整数来进行运算。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hiker帝国

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值