为什么别再用浮点数了

1,278 阅读10分钟

本文不仅仅适用于java

问题

先上两段代码,经过本文的讲解后,你应该能深刻的理解浮点数并且可能不想用"浮点数"(float/double)了

问题1

        float a = 0.1f;
        float b = 0.2f;
        float c = a +b;
        System.out.println(Float.compare(0.3f,a+b));
        System.out.println(c-0.3f);

        System.out.println("===================");

        double a1 = 0.1;
        double b1 = 0.2;
        double c1 =a1 +b1;
        System.out.println(Double.compare(0.3d,c1));
        System.out.println(c1 -0.3);
        System.out.println(Math.pow(2,-54));

输出

true
0.0
===================
false
5.551115123125783E-17
5.551115123125783E-17

为什么float 的0.3相等,而double的0.3不想等,相差了Math.pow(2,-54)

问题2

        float x = 0.0f/0.0f;
        System.out.println(x);

输出

NaN

为什么输出的是NaN,而不是报java.lang.ArithmeticException: / by zero这个异常

IEEE 754

我们知道任何数据在内存中都是以二进制存储的,所以IEEE 754 规定了浮点数在计算机中二进制的存储方式,java语言也遵守这一标准(哪个流行语言敢不遵守)。

浮点数类型

单/双精度浮点数分别对应java语言中的float和double

单精度浮点数

双精度浮点数

可以从上面的图可以看出,在 IEEE 754标准下,浮点数的二进制格式分为三部分,类似于科学计数法。

S代表符号位,0代表正,1代表负 E代表指数,b代表指数有多少位 M为尾数,n代表尾数有多少位

二进制数转换为浮点数

从二进制数转换为浮点数的公式如下

上面的公式有2个点,大家可能不理解,但是它的设计巧妙十分巧妙

第一,为什么E需要减去一个掩码的大小?因为E生成的时候加上了一个掩码的大小。

那么,为什么要加上一个掩码的大小呢? 首先我们的指数是有正负的,所以E的最高位为符号位,以8位的E为例子,使用使用普通的符号数,它能表示数字范围为 +0 ~ 127,-0~ -127,而对于一个数来当指数来讲,我们不需要2个0。所以通过加上一个掩码的大小(专业术语叫做移位存储)来解决这个问题。通过移位存储,我们能表达的数字范围为0 ~ 128,0~ -127 。

移位存储你也可以理解为用无符号数来映射有符号数

第二,为什么M前需要加上1?因为尾数M省略了一位。经过移位后,第一位强制为1,所以省略了。n位的尾数实际上是n+1位。 第一位强制为1?那不是不可能代表0了,浮点数的0怎么表示?这个下面再讲。

浮点数转换为二进制数

整数部分,除以2,不断取余数的1和0,结果倒序排列 小数部分,乘以2,不断取整数的1和0,结果正序排列

以10.5,10.2 ,0.2 和 0.5举例子 对于整数部分的10来讲,我们可以得到它的二进制为1010 对于0.5来讲

0.5 * 2 = 1.0 取1

所以0.5的二进制为.1

对于0.2来讲

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.2的二进制为 .001 1001 1001 1001...

规范起见,我们使用到的浮点数二进制需要通过幂次进位保证整形部分必须为1

以32位的单精度浮点数举例

10.5 的二进制表示为 0 10000010 0101 0000 0000 0000 0000 000 10.2 的二进制表示为 0 10000010 0100 0110 0110 0110 0110 011 0.5的二进制表示为 0 01111110 0000 0000 0000 0000 0000 000 0.2的二进制表示为 0 01111100 1001 1001 1001 1001 1001 101

转换代码

人肉转换太痛苦了,上代码

    public static void main(String[] args) {

        getFloatBin(0.1f);
        getFloatBin(0.2f);
        getFloatBin(0.3f);

        getDoubleBin(0.1d);
        getDoubleBin(0.2d);
        getDoubleBin(0.3d);
    }

    public static void getFloatBin(float f){
        int b=Float.floatToIntBits(f);
        String s = Integer.toBinaryString(b);
        int size = s.length();
        for(int i =0 ;i < 32-size;i++){
            s = "0" +s;
        }
        for(int i =0 ;i< s.length();i++){
            if(i ==1 || i ==9){
                System.out.print(",");
            }
            if(i >8 && (i-9)%4 ==0){
                System.out.print(" ");
            }
            System.out.print(s.charAt(i));
        }
        System.out.println();

    }

    public static void getDoubleBin(double d){
        long l = Double.doubleToLongBits(d);
        String s = Long.toBinaryString(l);
        int size = s.length();
        for(int i =0 ;i < 64-size;i++){
            s = "0" +s;
        }
        for(int i =0 ;i< s.length();i++){
            if(i ==1 || i ==12){
                System.out.print(",");
            }
            if(i >12 && (i-12)%4 ==0){
                System.out.print(" ");
            }
            System.out.print(s.charAt(i));
        }
        System.out.println();
    }

可以看出一个规律,只要不是5结尾的浮点数,对应的二进制都是无限循环,必须做舍入,二进制的舍入很简单,只要下一位是1,就进位

特殊数值处理

上面讲到指数的E的取值范围是0 ~ 128,0~ -127,但是 128(11111111) 和 -127(00000000) 这两个指数对应的浮点数有特殊的含义。

+0

-0

+∞

-∞

NAN

NAN = not a number ,浮点数的除以0操作会返回这个

非规范化数

除了+/-0以外,E=00000000的浮点数用于表示一些很小,很接近于0的数字,由于这个数省略了0开头所以叫非规范化数,对应省略1开头的叫规范化数。知道有这个存在即可,基本用不上。

浮点数的范围

知道了浮点数的二进制格式,那么它的范围也是可以轻松推出来的

规范化-最小值-绝对值

00 80 00 00 = 2-126 * (1+0/223)= 2-126 ≈ 1,17549435∙e-38 80 80 00 00 = -2-126 * (1+0/223)=-2-126≈ -1,17549435∙e-38

规范化-最大值-绝对值

7F 7F FF FF = 2127(2-2-23) = 2128≈ 3,40282347∙e+38 FF 7F FF FF = -2127(2-2-23) = -2128≈ -3,40282347∙e+38

非规范化浮点数就不列了,自行研究

解答开篇问题

问题1

对于单精度浮点数,也就是float,0.1+0.2计算逻辑如下

0.1 = 0,01111100,1001 1001 1001 1001 1001 101
	= 1,1001 1001 1001 1001 1001 101  * 2^-4
0.2 = 0,01111100,1001 1001 1001 1001 1001 101
	= 1,1001 1001 1001 1001 1001 101  * 2^-3
0.1 + 0.2 =  
             1,1001 1001 1001 1001 1001 101 * 2^-4 +
	        11,0011 0011 0011 0011 0011 01 * 2^-4
	      = 100,1100 1100 1100 1100 1100 111 * 2^-4
	      =  1,0011 0011 0011 0011 0011 001(1) * 2^-2
	      =  1,0011 0011 0011 0011 0011 010 * 2^-2
0.3 = 0,01111101,0011 0011 0011 0011 0011 010 
	= 1, 0011 0011 0011 0011 0011 010 * 2^-2

对于双精度浮点数,也就是double类型,计算逻辑如下

0.1 = 0,01111111011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
      = 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4
0.2 = 0,01111111100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
      = 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2 ^ -3
0.1 + 0.2 = 
        1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4 +
       11,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010 * 2 ^ -4
    = 100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1110 1100 1100 1110 * 2 ^ -4
    =   1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (10)  * 2 ^ -2
    =   1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100  * 2 ^ -2

0.3 = 0,01111111101,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
      = 1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011  * 2 ^ -2

浮点数转换为二进制后,存在无限循环的问题,因此到达有效位后会进行舍入,对于二进制数是1进位0不进位的规则。

在32位浮点数的 0.2 +0.1中,0.1,0.2,0.1+0.2,0.3,都是存在舍入操作的并且进位一样多。 而在64位浮点数的0.2+0.1中,由于有效数字是52位,刚好是4的倍数,并且后面接的二进制0011不会产生进位,但是在0.1 + 0.2 的过程中由于指数不对齐产生了进位,才导致0.1+0.2大于了0.3。

问题2

java语言的浮点数实现是遵循了IEEE 754规范,而NAN也是这个规范的要求之一。所以对于浮点数的除以0操作是会返回NAN的。

如何在java程序中处理浮点数

经过上面的分析,显而易见,如果你在java应用中使用float 或者 double类型进行运算,肯定是会存在精度问题的。

所以在应用中关于金额的字段务必使用BigDecimal或者使用整型进行操作!!!

如果不进行运算操作,还是可以照常使用的

至于BigDecimal的原理就是,整型在转化为二进制的时候不会存在精度问题。

参考

IEEE 754 标准
IEEE 754浮点数标准详解
为什么说浮点数缺乏精确性? python中浮点数运算问题
浮点数在内存中的表示移位存储难点的理解

看到这边,能否关注下我们的公众号