求求你,不要再让浮点数背锅了

缘起

今天刷技术公众号,看到了骇人听闻的标题: 踩坑了,BigDecimal使用不当,造成P0事故。点进去一看,影响到了核心链路上的收银台,P0不冤。
原文链接: https://juejin.cn/post/7087404273503305736

引出问题

作者写了示例代码,直接复现了问题。如下图所示。


问题复现

一句话,错误的使用了BigDecimal(double)的构造函数导致的。直接打开Double源码,源码作者也是用心良苦,直接警告了调用者,这玩意不准,大家要小心,还贴心地给了方法如何规避这个问题。如下图所示。


JDK中Double源码

写代码试了下,在阿里巴巴检查插件(Ali-Check)中直接提示该问题:
IDEA插件检查报错

心生疑问,为什么传入double就不准了呢?为什么会出现那么多位的不可描述的数字呢?复习下二进制小数的表达和存储就明白了。

浮点数详解

1、数学上二进制小数的表达

大家都知道,十进制数字的表达式:d = \sum^{m}_{i = -n}{10^i \times d_i}
其中,m为整数部分的位数,n为小数部分的位数;di是0~9的整数。
比如:1230.56 = 1\cdot10^3 + 2\cdot10^2 + 3\cdot10^1 + 0\cdot10^0 + 5\cdot10^{-1} + 6\cdot10^{-2}

以此类推,二进制数字的表达式:d = \sum^{m}_{i = -n}{2^i \times d_i}
其中,m为整数部分的位数,n为小数部分的位数;di是0~1的整数。
比如:10.01_2 = 1\cdot2^1 + 0\cdot2^0 + 0\cdot2^{-1} + 1\cdot2^{-2}

知道了这个,那问大家一个问题。0.1用二进制中如何表达呢?套用上面的求和表达式,似乎要先变成d\cdot2^i(d=0,1)的求和形式才行。
教大家一个方法,就是不停的乘以二进制的2(类似于不停的用10乘以10进制下的小数),以得到小数点后一位上的值。

推倒过程分解:
第1位:0.1\cdot2=0.2 --> 乘积小于1,故为0 --> 得到小数0.0
第2位:0.2\cdot2=0.4 --> 乘积小于1,故为0 --> 得到小数0.00
第3位:0.4\cdot2=0.8 --> 乘积小于1,故为0 --> 得到小数0.000
第4位:0.8\cdot2=1.6 --> 乘积大于1,故为1 --> 得到小数0.0001 (注意,这里要减去1后重新来过)
第5位:0.6\cdot2=1.2 --> 乘积大于1,故为1 --> 得到小数0.00011
第6位:0.2\cdot2=0.4 --> 乘积小于1,故为0 --> 得到小数0.000110(大家发现没有,到这里其实跟第2位的计算一样,开始无限循环了)
故最终的二进制表达式为:0.00011[0011](后面的中括号中无限循环)

惊不惊喜,意不意外,0.1这么简单的数字,在二进制中居然无限循环。如果再复杂点的数字,也就更加不可预测了。
当然,也有满足条件的小数是能被二进制准确(有限位内)表示的。条件就是它能够被表示成d = \sum^{m}_{i = -n}{2^i \times d_i}的形式。比如:63/64,26/128。发现没?都是2的幂为底的分数。在实数的世界里,沧海一粟。

数学上可以有无限循环的概念去表达一个小数,但是计算机的存储长度是有限制的(float是32位,double是64位),也就无法准确表达这个0.1了,即使是能够准确表达的小数,如果位数太长,由于计算机位数的限制,也是会被截断的。

可以想象,在浮点数的计算过程中,势必会有截断(向上舍入、向下舍入、向偶舍入、向零舍入)的操作,那计算结果自然会存在精度的问题了。现在再回想下,计算机计算出上面的结果:8.8000000000000000000007105427......也就不奇怪了。再把这个值直接传给BigDecimal(用double入参的方式),那BigDecimal也表示很无奈。

试试看 动动小手,看看63/64和26/128表示成二进制该如何写?

2、计算机中浮点数的存储

计算机中使用的是IEEE浮点标准,这个标准统一了不同机器上的浮点数存储标准。下面说说这个详细的存储方法。
IEEE浮点标准用V=(-1)^s\times M \times 2^E的形式来表示一个数。

  • 符号s(sign)决定是正数(s=0)还是负数(s=1);
  • 有效数M(mantissa)是一个二进制小数,它的范围在[1,2)或者[0,1)之间;
  • 指数E(exponent)是2的幂(可能是正数,也可能是负数);

下图表示了float和double的存储格式:


浮点数的存储示意图

按照E的不同取值,分为三种情况。
1、格式化值
E既不是全是0,也不是全是1时,属于格式化的值。此时exponent的位数中不再有符号位,也就是无法天然地表达正负数。于是就引入了偏置(biased)的概念。也就是说E=e-biased(这里e表示exponent区域的二进制表达的实际数字),而真正的指数值E需要减去偏置(biased)。对于float,biased=127;对于double,biased=1023。

对于有效数字M,其取值范围是[1,2)。由于二进制的小数表达中,第一位一定是1,比如正常的二进制科学计数表达式:1.01000101 \times 2^6。为了节省这一位,这个1也就不会保存在M中。换言之,其存储的是小数点以后的二进制数,隐藏了1。

2、非格式化值
E全是0时,表示非格式化的值。此时,E=1-biased;同时M的范围变成了[0,1),也不会再隐藏1。

非格式化值的2个作用如下所述:

  • 表示0(有意思的是IEEE标准中,既有+0,也有-0,且二者不等)
    float的+0的格式:0 00000000 00000000000000000000000
    float的-0的格式:1 00000000 00000000000000000000000
    double一样,这里不赘述。
  • 表示非常接近0的数
    这个非常近怎么衡量呢?对于float来说,就是小于2^{-127};对于double来说,就是小于2^{-1023};因为如果数字比这个还小,那么E由于存储位数的限制是无法表达的。

3、特殊值
E全是1时表示特殊值。

  • 正无穷: s=0,E全是1,M全是0;
  • 负无穷: s=1,E全是1,M全是0;
  • NaN(Not a Number):E全是1,M不为0;

举个栗子
float f = 0.1f; int ifv = Float.floatToIntBits(f); System.out.println(Integer.toBinaryString(ifv));
此时输出:
111101110011001100110011001101

补足32位,并按照1、8、23来划分开,分别计算s、E和M。
0 01111011 10011001100110011001101
s=0
E=123-127=-4
M=1.10011001100110011001101 \times 2^{-4} = 0.000110011001100110011001101

我们上面计算过0.1的二进制表达:0.00011[0011](后面的中括号中无限循环)
看吧,对上了,明显计算机做了截断。

回顾

今天我们学习了2个内容:
1、数学上小数的二进制表达方法。
2、计算机中二进制浮点数的保存标准。

结论

  • 计算机世界中二进制并不能精确表达所有浮点数;浮点数计算存在精度丢失。
  • 科学计算是可以用浮点数运算;金融计算用BigDecimal。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容