关于浮点数的这个问题你会吗?— 500000000f < 500000000f + 1 ?

public boolean isXXX(){
  if (500000000f < 500000000f + 1){
    return true
  }
  return false
}

如果有以上的一段代码,那结果得到的是true还是false呢?

这里先留个疑问,我们先来研究一下,浮点数是咋回事。

浮点数带来的问题

我们来看看这该死的浮点数平时会给我们码农带来什么样的困扰吧。

相信大部分程序员都知道浮点数在表示小数的时候,会有精度问题,例如0.1+0.2,如此简单的计算,计算机竟然得到这样的结果0.30000000000000004,所以在计算金钱的时候我们经常会使用 BigDecimal 类处理金额,或者全都转换成整数计算,最后展示的时候再缩小数值倍数,保留小数。

还有标题中提到的问题,用float存储整数,对整数做加法,之后再做比较,会有问题吗,答案是肯定的。这个问题比小数计算丢失精度更严重,有可能在一个逻辑判断中执行了不该执行的逻辑。而且经验不足的情况下,单从代码上很难找到bug的原因。

除了以上两个问题,浮点数还有很多问题,例如

  • 在Java中,float占用4个字节,long占用8个字节,但是float能表示的数值范围却要比long的范围还要大,为什么呢?
  • 浮点数0.1,在计算机中存储的值真的是0.1吗?
  • 什么是IEEE754标准?

让我们来依依把这些问题解释清楚。

浮点数的进制转换

正整数的转换

我们先来点简单的,做一个正整数的十进制转二进制,我们举几个例子

十进制 2为底指数表示 二进制
1 20 1
2 21 10
3 20+21 11
4 22 100
8 23 1000
16 24 10000

其实二进制很简单,就是逢二进位么。但是也有一些规则,比如把正好能表示成 2x的十进制数值转成二进制,正好就是在1后面补x个0。这就导致十进制的*2和÷2计算,可以用二进制的移位来替代,在有些算法中还是挺常用的。

我们继续,聪明的人类于是总结了一套十进制转二进制的计算公式,先看下下面这个图,你们是否还记得,这是我们小学的除法竖式计算方法。

image.png

而十进制转二进制的计算方式跟这种方式很类似,我们把46转成二进制,是这样的:

image.png

其实就是把46除以2得到商23和余数0,然后对商继续除以2,得到商11余数1,依次类推,直到商为0,这个时候把得到的余数倒序排列,就得到了二进制 101110。

到这里,正整数的转换我们已经会了,现在我们来看下小数的转换。

小数的转换

小数的转化在理解上比正整数要绕一些,我们先思考一个问题。

十进制1等于二进制的1,那么十进制的0.1是不是等于二进制的0.1呢?

那肯定不对,我们要先理解.的含义,在正整数转换的时候,我们会时刻提醒自己进制的存在,在考虑小数的时候很可能会忽略了.的含义在十进制和二进制中是不一样的。其实.就是用.后面的数除以进制数,比如十进制的0.1就是1÷10,那么二进制的0.1呢,其实是1÷2,也就等于十进制的0.5,所以小数的转换中,二进制的0.1等于十进制的0.5 。

理解了这个,我们就能列出一些小数的转换关系

十进制 以2为底的指数表示 二进制
0.5 2-1 0.1
0.25 2-2 0.01
0.75 2-1+2-2 0.11
0.125 2-3 0.001
0.875 2-1+2-2+2-3 0.111

于是聪明的人类就又总结出了小数的计算公式,假如我们计算0.625的二进制,那么计算过程就是这样的:

image.png

计算过程为:用0.625乘以2,截取整数部分作为结果的一部分,取小数部分继续乘以2,依次类推,直到结果正好等于1,然后把所有的截取的整数部分正序排列,前面补上0.,就是二进制的结果0.101 。

好,重点来了,通过这两个公式你会发现一个问题,就是正整数的时候,永远都能计算出一个二进制正好等于十进制。但是小数就不一定了。比如计算一下0.1的二进制形式

那么计算过程就是这样的

本次待计算的值 乘以2以后得值 乘以2以后的小数部分 截取的整数部分
0.1 0.2 0.2 0
0.2 0.4 0.4 0
0.4 0.8 0.8 0
0.8 1.6 0.6 1
0.6 1.2 0.2 1
0.2 0.4 0.4 0

发现了吧,这样计算会一直算下去,没有穷尽,这个普通的十进制小数,在二进制的世界里变成了无限循环小数。这就造成了二进制表示小数会出现精度不准的问题,以及计算的时候会造成结果不准确。

那么二进制可以准确的表示出多少十进制的小数呢?答案是非常少。

List<String> numList = new ArrayList<String>();
for (int i = 1; i < 100000; i++) {
    String tmpNum = new BigDecimal(i / 100000f).toString();
    if (tmpNum.length()<=7){
        numList.add(tmpNum);
        System.out.println(tmpNum);
    }
}
System.out.println(numList.size());

=================
0.03125
0.0625
0.09375
0.125
0.15625
0.1875
0.21875
0.25
0.28125
0.3125
0.34375
0.375
0.40625
0.4375
0.46875
0.5
0.53125
0.5625
0.59375
0.625
0.65625
0.6875
0.71875
0.75
0.78125
0.8125
0.84375
0.875
0.90625
0.9375
0.96875
31

我们写这样一段代码,代码中tmpNum长度大于7的都是不能准确表示的浮点数,可以看到,从0.99999-0.00001这个范围内,只有31个数可以准确表示出来。

好说明白了进制转换的问题,我们再来看二进制小数是怎么在计算机中存储的。

IEEE754标准

在计算机的世界里是用二进制存储任何数据,小数也不例外,那么小数转成二进制我们知道怎么转了,那么存储的时候,是直接把转换的二进制存储到计算机中吗,答案是否定的。因为在二进制的世界里,没有.这个符号,所以要制定一定的格式来存储。

十进制中任何数字都可以转成指数形式,例如300.02可以写成3.0002*102 ,在存储小数的时候,就是运用了这种方式。

例如我们要存储111.01,就可以看成存储的是 1.1101*22 ,其中1.101是要存储的数值,指数2是要存储的指数值。这两个值知道了,小数整体的值就可以计算出来了。

浮点数存储结构

我们以4字节的float为例,看下具体的储存结构。

image.png

4个字节的情况下,会使用第1位作为符号位,表示正负值,后面的8位存储指数,再后面的23位存储的是有效的数值。以1.1101*22为例,其中符号位是0,指数位是2,有效数是1.1101,他们分别被存储在自己的区域内。

但是在各个区域具体存储的时候,还不是直接把值存进去,而是有一定的规则。

符号位

首先符号位,没有特殊规则,就是0代表正数,1代表负数。

指数域

指数域的值就有一些规则了,在4字节的float类型中存储1.1101*22,指数域部分并不是存储的2,而是127+2,要以127作为一个偏移量,待保存的指数加上这个偏移量才是最后存入指数域的数值。如果是浮点数的指数部分是2-2 ,实际上保存的是127-2,也就是125,这样就不需要存储负数了,不管指数是正的还是负的,指数域存储的都是正数。这个偏移量是存储数据的指数域位数决定的,具体公式是2指数域位数-1 ,4字节float是用8位做指数域,计算出来也就是127 。

有效位数域

有效位数域规则就更多复杂一点。在说有效位之前,先说一下,二进制浮点数的分类,可以分为3类

  • 正规化浮点数
  • 非正规化浮点数
  • 特殊浮点数

特殊浮点数

特殊浮点数其实就是特殊的几个数:0,正无穷,负无穷,NaN。其中0比较特殊,有两种表示形式,+0和-0,他们的具体表示如下:

image.png

正规化浮点数

然后再来说正规化浮点数,正规化浮点数就在存储1.1101* 22这种小数的时候,指数按照之前的逻辑,存储为129,有效位只存储小数部分,也就是0.1101,当取值的时候,默认在前面+1 。这样做的好处是节省了存储数据的位数,但是限制了最小的可以表示的小数。

我们用1.1101* 22举个例子,

符号位 指数域 有效位数域
0 127+2 11010000000000000000000

我们想一下1.0* 2-127 这个数如果按照正规化浮点数的方式存储在计算机中是什么样子,没错,符号位,指数域,有效位数域,全都是0,这就和特殊浮点数0冲突了,所以按照正规化存储浮点数理论最小值应该是1.00000000000000000000001* 2-127 那如果想再比这个值小怎么办呢?于是就有了非正规化浮点数。

非正规化浮点数

在非正规化浮点数中,指数域固定是全0,指数值固定为1-偏移量,4字节float中也就是-126,同时有效位数域不需要再+1,所以可以表示的最小值变成了

0.00000000000000000000001* 2-126 ,这个值要不上面提到的1.00000000000000000000001* 2-127小了好多好多。

0.00000000000000000000001* 2-126 表示如下:

符号位 指数域 有效位数域
0 0 00000000000000000000001

这个缝缝补补的浮点数存储的方案,就是IEEE754标准了。

问题解答

现在我们回过头来看看为什么 500000000f < 500000000f + 1 不成立。

根据之前十进制转二进制的公式可以计算得到500000000f的二进制值是11101110011010110010100000000,使用指数方式表示为 1.1101110011010110010100000000* 228,按照IEEE754标准存储,符号位:0,指数域:155,有效位数域就出现问题了,小数点后面有28位,有效位数域只有23位,所以只能舍弃到最后的5位数值,存储的值为11011100110101100101000,这个时候如果对数值做加1计算,以指数形式看,等于1.1101110011010110010100000000* 228(500000000f) + 0.0000000000000000000000000001* 228(1),但是还记得吗,计算机里存储的值,已经把最后的5位给舍弃掉了,所以加了1之后,再存储到计算机中,等于没有变化。所以上面的比较是不成立的。具体值的转化关系如下:

十进制 二进制 指数形式 符号位 指数域 有效位数域
500000000f 11101110011010110010100000000 1.1101110011010110010100000000* 228 0 127+28 11011100110101100101000
500000001f 11101110011010110010100000001 1.1101110011010110010100000001* 228 0 127+28 11011100110101100101000

浮点数的间隙

通过上面的表格我们知道,需要有效位数域或者指数域有变动,那么浮点数才会变化,那与500000000f最近的下一个计算机能表示的浮点数是多少呢?其实就是把有效位数域的值加1

十进制 二进制 指数形式 符号位 指数域 有效位数域
500000000f 11101110011010110010100000000 1.1101110011010110010100000000* 228 0 127+28 11011100110101100101000
500000032f 11101110011010110010100100000 1.1101110011010110010100100000* 228 0 127+28 11011100110101100101001

也就是500000000f到500000032f之间的数,计算机都不能表示,这个差值,就是浮点数的间隙。间隙不是固定的,是随着浮点数值越大变的越大。

浮点数的舍入原则

上面说到了浮点数的间隙,那么在浮点数间隙中的数值怎么在计算机里表示呢,比如500000001f,500000002f,500000003f,500000029f,500000030f,500000031f这些数。答案是没有办法,就找最近的数存下来,500000001f,500000002f,500000003f就直接存成了500000000f,500000029f,500000030f,500000031f就存成了500000032f,这就是浮点数的就近舍入。

那如果是离左右两次的值一样远的数呢,比如500000016f,他与500000000f和500000032f的距离一样远,那么会存储为两个间隙值中有效位数域中尾数是0的那个数值,从上面的表格中可以看到,500000000f的有效位数域的尾数是0,所以500000016f就表示成了500000000f。这就是舍入到偶数原则。

从代码中也可以印证这样的结论

System.out.println(500000001f == 500000000f);
System.out.println(500000016f == 500000000f);
System.out.println(500000016f == 500000032f);
System.out.println(500000017f == 500000032f);
=============
true
true
false
true

到这里关于浮点数的常见问题我们应该都能找到答案了。希望对你的学习有帮助,如果错误欢迎指出。

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