在日常的开发中,经常会出现一些因为使用double类型而导致的精度丢失问题。那么问题来了,精度丢失问题到底是一个偶发问题还是一个必现问题呢?下面我们一起来仔细分析分析。
我们先来看一段代码:
double d1 = 20.3;
double d2 = 6.9;
System.out.println(d1+d2);
我得到了一个执行结果
27.200000000000003
OK巧了么,精度丢失他来了。无论我执行多少遍,他依然是这个数字。
这时候,我们祭出开发大法,我们用BigDecimal来做double的加法运算,这样就不会精度丢失了吧,看我的:
double d1 = 20.3;
double d2 = 6.9;
BigDecimal b1 = new BigDecimal(d1);
BigDecimal b2 = new BigDecimal(d2);
BigDecimal b3 = b1.add(b2);
System.out.println(b3);
System.out.println(b3.doubleValue());
执行结果:
27.2000000000000010658141036401502788066864013671875
27.200000000000003
卧槽!为什么精度仍然丢失了?这和预想的根本不一样啊!为什么使用BigDecimal用于double运算仍然无法解决double精度丢失的问题?
接下来,我们仔细分析一下,为什么会有精度丢失这个问题产生。
double,又称双精度浮点数。这时候引荐一位它的朋友,float,单精度浮点数,用来做案例分析。
要弄明白为什么会有精度丢失这个问题的产生,我们首先要明白,计算机是如何存储float和double这类数据的。
众所周知,计算机是以二进制数来做数据存储的,一个浮点数的数据结构由以下部分构成:
符号位、阶数位 和 小数位。
float由32个二进制数构成,double则由64个二进制数构成(有兴趣的同学可以调用Float.SIZE和Double.SIZE进行实验验证)。
float和double的存储数据结构如下:
float存储结构:
0(符号位1位)0000 0000(阶数位8位)0000 ... 0000(小数位23)
** double存储结构:**
0(符号位1位)0000 0000 000(阶数位11位)0000 ... 0000(小数位52)
这里就需要解答一下基础比较薄弱的同学的提问:这三部分结构分别代表什么意思?一个数字是如何被表现出来的?
计算机在表示浮点数的时候,采用的是科学记数法,我们从十进制数开始举例:
当我们需要表示一个十进制数 12345 那么我们会表示成1.2345*10^4
这个地方 我们其实省略了一个符号位,就是写全是:1*1.2345*10^4
。这里1 代表符号位,如果数字为负,那么我们就换成-1,1.2345代表有效数字,十的四次方的^4
就是阶数位,也就是12345用科学记数法来表示,就是符号位:1、阶数位:4 和 小数位:1.2345。
OK,搞明白了上面这一段,我们把思想转换为二进制数。
以float来举例:
第一部分符号位用于表示数字的正负;
第二部分阶数位用于表示这个数字需要乘以多少个2;
第三部分小数位用于表示这个数字的有效数字。
好了,聪明的同学已经得到了一个结论了,float的有效数字是有限的,具体多少,自己计算(百度一下),得到16777215这个数字。这时候就又有同学提问了:你这不对啊,float明明可以表示(-3.4E+38)~(3.4E+38)之间的数字啊,你这怎么才这么点?
你说的对,但是也需要注意,你说的是取值范围,我说的是精度。
举个例子:
float能够精确的表示:16777215这个数字,但是无法精确的表示16777216这个数字;
我们用控制台输出16777216和16777217 得到的都是16777216这个数字,也就是说,你看见的,不准了。
取值范围(数字大小),精度(有效数字),你品,你仔细品。
好的,说了半天,也就说了个取值范围和精度的事儿,我一个20.3+6.9,关你上面这啥事儿了?
好的,你把砖头放下,我们好好继续往下唠。
刚刚我们只说了小数位之前的数字是如何表示的,小数点之后的数字,我们是怎么表示的呢?
还是以十进制举例:
0.12345
我们可以写成12345*10^-5
,也就是说是12345/10^5
换句话说,我们想得到一个没有小数点的数12345(填到有效数字坑上面的数字),我们需要拿他的小数部分乘以进制(10),这里我们乘一下,来得到十进制里有效数字的书写过程:
0.12345 * 10 1.2345 1
0.2345 * 10 2.345 2
0.345 * 10 3.45 3
0.45 * 10 4.5 4
0.5 * 10 5 5
这时候,我们请抄起搬砖的同学先坐下,我之所以这么写,是为了让大家明白,整数位和小数位的表示方式是有那么一丝丝的区别的。
这里我们转换一下,我们用二进制来表示0.125
0.125 * 2 0.25 0
0.25 * 2 0.5 0
0.5 * 2 1 1
也就是说,0.125用二进制表示,那么他的小数位写法是001
OK,啰嗦了那么多,我们来看看,计算机到底是如何表示20.3的:
首先,小数位之前的数字20,我们用十进制表示为10100
计算过程:
除法 是否有余数
20/2 0
10/2 0
5/2 1
2/2 0
1/2 1
这时候,小数部分是0.3
0100 1100 1100 1100 ...
计算过程:
0.3*2 = 0.6 0
0.6*2 = 1.2 1
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.4*2 = 0.8 0
--------------
... 1
... 1
... 0
... 0
--------------
它无限循环了
然后我们发现,这23位有效数字根本不够填,最后我们没有办法,填入了10100 0100 1100 1100 1100 11。好的,机智的同学已经发现了,这不准确!float居然无法准确的表示20.3这个数字!那double也同理,无法准确的表示20.3这个数字。而且我们执行一下如下代码来验证一下:
double d1 = 20.3;
float f1 = 20.3f;
System.out.println(d1 == f1);
果然得到结果 false
。至于为什么,想必大家已经十分明了了,OK,那么回到文章开头的问题:
20.3都无法准确的表示,你再来问我为什么20.3+6.9为什么会得到那样的结果,是不是就过分了呢同学?
好,我们再来看另一个细节性的问题,在我们的印象中,BigDecimal用来做double运算,是能够解决我们遇到的问题的,那么为什么开头的运算无法得到我们想要的答案呢?
这里给大家一段代码,大家执行一下,然后评论区来告诉我答案:
double d1 = 20.3;
double d2 = 6.9;
BigDecimal b1 = new BigDecimal(d1+"");
BigDecimal b2 = new BigDecimal(d2+"");
BigDecimal b3 = b1.add(b2);
System.out.println(b3);