likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 3
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为底指数表示二进制
12^0^1
22^1^10
32^0^+2^1^11
42^2^100
82^3^1000
162^4^10000

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

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

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

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

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

其实就是把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.52^-1^0.1
0.252^-2^0.01
0.752^-1^+2^-2^0.11
0.1252^-3^0.001
0.8752^-1^+2^-2^+2^-3^0.111

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

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

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

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

那么计算过程就是这样的

本次待计算的值乘以2以后得值乘以2以后的小数部分截取的整数部分
0.10.20.20
0.20.40.40
0.40.80.80
0.81.60.61
0.61.20.21
0.20.40.40

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

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

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*10^2^ ,在存储小数的时候,就是运用了这种方式。

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

浮点数存储结构

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

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

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

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

符号位

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

指数域

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

有效位数域

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

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

特殊浮点数

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

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

正规化浮点数

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

我们用1.1101* 2^2^举个例子,

符号位指数域有效位数域
0127+211010000000000000000000

我们想一下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^ 表示如下:

符号位指数域有效位数域
0000000000000000000000001

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

问题解答

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

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

十进制二进制指数形式符号位指数域有效位数域
500000000f111011100110101100101000000001.1101110011010110010100000000* 2^28^0127+2811011100110101100101000
500000001f111011100110101100101000000011.1101110011010110010100000001* 2^28^0127+2811011100110101100101000

浮点数的间隙

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

十进制二进制指数形式符号位指数域有效位数域
500000000f111011100110101100101000000001.1101110011010110010100000000* 2^28^0127+2811011100110101100101000
500000032f111011100110101100101001000001.1101110011010110010100100000* 2^28^0127+2811011100110101100101001

也就是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

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