谁偷走了我的精度

在日常的开发中,经常会出现一些因为使用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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,563评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,694评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,672评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,965评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,690评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,019评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,013评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,188评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,718评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,438评论 3 360
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,667评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,149评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,845评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,252评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,590评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,384评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,635评论 2 380

推荐阅读更多精彩内容