CC BY 4.0(除特别声明或转载文章外)
学业繁忙,近来才刚有时间开始学java,其中又一次接触到了浮点数这个玩意儿,联系以前上学期matlab的经验,打算写篇博客总结下我目前对浮点数的粗浅理解。
一、什么是浮点数
我们都知道,计算机内部是0和1组成的二进制,不存在第三种状态用以表示小数点,所以要表示小数只能通过事先规定,比如固定哪部分为小数哪部分为整数:01001000,规定其首位是符号位,之后三位为整数位而最后四位为小数位。
但这么做会产生一个问题,同大小空间下能表示的数的范围和精度都不高,造成空间的浪费。为了提升能表示数的范围,就将整数部分规定为表示指数,小数部分维持原样,极大提升了能表示的数据范围和最小数的精度,这就是目前采用的浮点数。常见的浮点数类型有:float和double,其中float是1位符号位、8位指数、23位尾数,double是1位符号位、11位指数、52位尾数。
二、浮点数是一个个不均匀不连续的浮点
易证得,对于十进制中的任何实数,都能连续且唯一地对应其他进制的无穷等比数列求和式。也就是说,当小数部分为无穷多的时候,二进制和十进制能完全一一对应。但是,计算机不可能有无穷的空间存储小数部分,超出存储上限的小数部分被截断→这就是截断误差和浮点数不连续性的根本原因。
例如,float的1与2之间,由于存在23位尾数,所以最小可以精确到2的-23次方,但是2与4之间最小就只能精确到2的-22次方(由于小数部分全体乘上了指数部分)。这种进位当小数部分有无穷多的时候无伤大雅,因为不论怎么进位,无穷多也不会变回有限多,但浮点数由于小数的截断,当指数部分足够大时,进位下来相邻两个浮点数可能有高达几百几十的差距。这也是为什么一些对精确有高要求的数据不可能用浮点数来存放的原因,比如金额系统等。
三、大概有多不均匀?
类比matlab中的eps的概念,在java中也是一样的。
对于float而言,$2^0-2^1$之间,1的下一个浮点数是$2^0·(1+2^{-23})$,…,$2^n-2^{n+1}$之间,$2^n$的下一个浮点数是$2^n·(1+2^{-23})$。
double类似,只不过最小的尾数是$2^{-52}$。
四、在浮点数之间的数是如何处理的?
以相邻俩浮点数中间为界,就近舍入。
正好在中间的数,以float为例,令$eps = 2^{-23}$,经过idea编译运算总结出以下规律:
若$y=2^x+2^x·(n+0.5)·eps∈[2^x+2^x·n·eps,2^x+2^x·(n+1)·eps]$,则结果是n与n+1之中的偶数。
如:
public class Test {
public static void main(String[] args) {
float eps = 1.1920928955078125e-7F;//2^(-23)
float a = (float) (1 + 2*eps);
float b = (float) (1 + 3*eps);
float c = (float) (1 + 4*eps);
float d = (float) (1 + 2.5*eps);
float e = (float) (1 + 3.5*eps);
System.out.println(d==a);
System.out.println(d==b);
System.out.println(e==b);
System.out.println(e==c);
}
}
运行结果:
true
false
false
true
double类型有类似的结论。
五、为什么浮点数能表示的最小正数比精度小得多?
前面说了,浮点数是由指数乘小数部分加和而来的大小。而其中的指数部分,采用的是无符号形式减去偏差值的计算方法。以float为例的话,8位指数位无符号形式范围是:$[0,255]$,float的偏差值是127,则实际指数范围是:$[-126,127]$。
于是按正常情况而言,最小正数为$2^{-126}$(因为浮点数是先将二进制小数转换成科学计数法的形式,统一规格再去存储。这样所有的数字除了0之外,一定会含有一个1。默认把1加上这样就可以用23比特位存24位的数据。所以小数部分最小为$2^0$。)
如果去看jdk源码的话你会发现Float下的MIN_NORMAL确实是这个大小,但是还有个MIN_VALUE是$2^{-149}$。这是因为:
再加个规则吧。当时我们规定当E为0的并且M每一位全为0,我们就说这个数字是0。那M不全为零呢?
之前是假设省略了最高位1,我们加一个新规则,当E全为0时,我们就说最高位不再省略1。所以最小的尾数就可以是0.00…1了,小数点后22个零,转成十进制就是$2^{-23}$。指数还是之前的-126,所以比之前更小的正数就是$2^{-23}·2^{-126}=2^{-149}$。
由于对规则有所修改,故原规则下的最小正数取名为MIN_NORMAL而这真正的最小正数取名为MIN_VALUE。