JVM (2) : 浮点数

一、说明

实际编程中,经常会遇到浮点数,这里以float为例,简单介绍下浮点数的一些基本知识。

二、存储规范

2.1 概念

IEEE754标准规定,浮点数包含“符号Sign”、“指数Exponent ”和“尾数Mantissa”三部分。

java数据类型 符号位 指数位 尾数位 长度
float 1 8 23 32
double 1 11 52 64

float和double的尾数都含有一个隐含位,对于有限数,如果指数位不全为0 则尾数位首位 +1 ,如果指数位全为0,则尾数位首位 + 0,相当于多存储了一位数据。有限数的概念后面会介绍。

二进制浮点数实际表示为:

± X n X n − 1 . . . X 0 . X − 1 X − 2 . . . X − m = ± X n . X n − 1 . . . X 0 X − 1 X − 2 . . . X − m × 2 n ± X_nX_{n-1} ...X_0.X_{-1}X_{-2} ...X_{-m} = ± X_n.X_{n-1} ...X_0X_{-1}X_{-2} ...X_{-m} × 2^n ±XnXn1...X0.X1X2...Xm=±Xn.Xn1...X0X1X2...Xm×2n

其中,第一位为符号位。小数点. 之前的为整数位,小数点之后的为小数位。可以看出来,一个浮点数包含三个部分的数据要存储,一个符号位,一个数值部分,还有一个就是指数部分。下面我们以float为例,简单介绍下。

float规格化数据表示:

± 1. f × 2 E − 127 ± 1.f × 2^{E-127} ±1.f×2E127

其中±为符号位,0表示正数,1表示负数。

E为指数,存储在指数位,8位二进制,其中[ 1,127 ) 算出来指数真值是负数, [ 127, 254 ] 是正数,0和255有特殊用途。

f为尾数,非0场景下,二进制表示,整数位固化为1,省略掉,所以可以用23位指数位表示24位尾数。
实际10.25存储成二进制,就是0 1000 0010 0100 1000 0000 0000 0000 000,1 + 8 + 23 ​总共32位。​

2.2 示例

浮点数转换为二进制

以10.25为例,来说明下浮点数如何转化为二进制。

  • 整数部分10 转换为二进制 1010。
  • 小数部分0.25,使用“乘2取整”的方法。(0.25 * 2 = 0.5) , (0.5 * 2 = 1) ,1 最终为整数。所以0.25 转换成 01 。其实从上面二进制浮点数表示的公式也能够看出来,小数位的值相当于2^(-n),n从1开始,越往后表示的值越小。
  • 合并整数位和小数位,有 10.25 = 1010.01 = 1.01001*(2^3),3就是前面公式中的指数,对于float来说,这里的 3 = E -127,E就是130,转换为二进制位 1000 0010, 为实际指数位存储的值。
  • 尾数位存储的是01001,去掉了1.01001前面的1,也就是我们前面说的隐含位。此处尾数位不全为0,所以隐含位为1,不用存储。总共是23位,其他位补0,为0100 1000 0000 0000 0000 000
  • 这个数字是正数,所以最高位补0,最终10.25转换成的二进制为:0 1000 0010 0100 1000 0000 0000 0000 000,总共32位。

二进制转换为浮点数

以0 1000 0010 0100 1000 0000 0000 0000 000为例

  • 最高位为0,为正数。
  • 取8位指数位 1000 0010,得到E为130,实际指数真值130-127 = 3
  • 取后面23位0100 1000 0000 0000 0000 000,因为不全为0,首位补1,得到1.0100 1000 0000 0000 0000 000。
  • 参考前面的公式,有
    + 1.01001000000000000000000 × 2 130 − 127 = 1010.01000000000000000000 = 2 3 + 2 1 + 2 − 2 = 10.25 + 1.0100 1000 0000 0000 0000 000 × 2^{130-127} = 1010.0 1000 0000 0000 0000 000 = 2^3 + 2^1 + 2^{-2} = 10.25 +1.01001000000000000000000×2130127=1010.01000000000000000000=23+21+22=10.25

2.3 分类

从前面可以了解到浮点数的转化规则。实际上,浮点数包含多种分类。
IEEE标准定义了6类浮点数:

指数 分类 隐含位 尾数 说明
xx…xx 有限数 1 xx…xx 指数非全0,非全1,有隐含位
00…00 0 0 0 尾数为0
00…00 弱规范数 xx…xx 尾数不是0
11…11 ±∞ 1 0 尾数不为0
11…11 QNaN 1 1xx…xx 尾数高位为1,但尾数不为0
11…11 SNaN 1 0xx…xx 尾数高位为0,但尾数不为0

IEEE标准要求:SNaN参与运算触发异常,QNaN则不触发异常。两者的区别在于尾数最高位不同。

弱规范数
表示0和最小有限数之间的数,指数和0一样,都是全0,但没有隐含位,尾数部分不为0
± ( f ) × 2 0 − o f f s e t ± (f) × 2^{0-offset} ±(f)×20offset

​特殊指数位,以float为例,指数位0主要是表示0和弱规范数,指数位为255则用来标识NaN和±∞。

2.4 特殊数

最小的正float有限数

根据有限数的规格化形式,指数取最小值1,隐含位是1,尾数取最小值0(23位都是0)
1.0 × 2 1 − 127 = 1.1754943508222875079687365372222 e − 38 1.0 × 2^{1-127} = 1.1754943508222875079687365372222e^{-38} 1.0×21127=1.1754943508222875079687365372222e38

最大的正float有限数

根据有限数的规格化形式,指数取最大值254,隐含位是1,尾数取最大值(23位都是1):
1.11111111111111111111111 × 2 254 − 127 = ( 2 − 2 − 23 ) × 2 127 = 2 128 − 2 104 = 3.4028234663852885981170418348452 e 38 1.11111111111111111111111×2^{254-127} =(2-2^{-23})×2^{127} =2^{128}-2^{104} =3.4028234663852885981170418348452e^{38} 1.11111111111111111111111×2254127=(2223)×2127=21282104=3.4028234663852885981170418348452e38

最小的正float弱规范数

指数是全0,但没有隐含位,尾数取最小值(最后一位为1),尾数包含整数位。

0.0000000000000000000001 × 2 0 − 127 = 2 − 22 × 2 − 127 = 1.4012984643248170709237295832899 e − 45 0.0000000000000000000001×2^{0-127} =2^{-22} × 2^{-127} =1.4012984643248170709237295832899e^{-45} 0.0000000000000000000001×20127=222×2127=1.4012984643248170709237295832899e45

三、 java 浮点数相关

3.1 java 常用浮点数展示

以float为例

public class FloatTest {

    @Test
    public void test() {

        printFloatBitStr(10.25f);
        printFloatBitStr(-10.25f);

        // 最大的正float有限数
        printFloatBitStr(Float.MAX_VALUE);
        // 最小的正float有限数
        printFloatBitStr(Float.MIN_NORMAL);
        // 最小的正float弱规范数
        printFloatBitStr(Float.MIN_VALUE);
        // QNaN
        printFloatBitStr(Float.NaN);

        printFloatBitStr(10.25f);
        // 表示不了,变成10.25f了
        printFloatBitStr(10.250000059604645f);
        // 表示不了,变成10.250001f了
        printFloatBitStr(10.2500011f);
        
        printFloat("0 10000010 01001000000000000000000");
        printFloat("0 10000010 01001000000000000000001");

    }

    private void printFloatBitStr(Float f) {
        String fs = Integer.toBinaryString(Float.floatToIntBits(f));
        while (fs.length() < 32) {
            fs = "0" + fs;
        }
        System.out.println(fs.substring(0, 1) + " " + fs.substring(1, 9) + " " + fs.substring(9) + " = " + f );

    }

    private void printFloat(String fs) {
        System.out.println(fs + " = " + Float.intBitsToFloat(Integer.parseInt(fs.replaceAll(" ", ""), 2)) );
    }


}

输出结果如下:

0 10000010 01001000000000000000000 = 10.25
1 10000010 01001000000000000000000 = -10.25
0 11111110 11111111111111111111111 = 3.4028235E38
0 00000001 00000000000000000000000 = 1.17549435E-38
0 00000000 00000000000000000000001 = 1.4E-45
0 11111111 10000000000000000000000 = NaN
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000001 = 10.250001
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000001 = 10.250001

四、 精度丢失问题

4.1 分析

浮点数经常遇到精度丢失问题,以上面的10.25来说,二进制为 0 10000010 01001000000000000000000。
假设有另外一个二进制数,尾数位为 24位 010010000000000000000001, 其他位保持不变,则 这个数的值为:
+ 1.010010000000000000000001 × 2 130 − 127 = 1010.010000000000000000001 = 2 3 + 2 1 + 2 − 2 + 2 − 24 = 10.25 + 0.000000059604645 = 10.250000059604645 + 1.0100 1000 0000 0000 0000 0001 × 2^{130-127} = 1010.0 1000 0000 0000 0000 0001 = 2^3 + 2^1 + 2^{-2} + 2^{-24} = 10.25 + 0.000000059604645 = 10.250000059604645 +1.010010000000000000000001×2130127=1010.010000000000000000001=23+21+22+224=10.25+0.000000059604645=10.250000059604645
但是float决定了 尾数位 为23 ,所以最后一个1 也就没有了。也就是0 10000010 01001000000000000000000到0 10000010 01001000000000000000000之间的数,只要不能用弱规范数表示的,都表示不了了。

从前面的java示例中,我们也能够看出来10.25到10.250001之间的很多数据都表示不了了,比方说10.250000059604645f,就向下取了,变成了10.25。同样的,计算过程中会存在位移,也会产生同样的后果,最终导致在一些精度丢失的问题。

4.2 java中的规避方法

使用java.math.BigDecimal#BigDecimal(java.lang.String)规避精度丢失问题

 @Test
    public void test2() {
        // 此处以双精度 double为例,计算过程中,会存在位移,导致精度丢失。
        System.out.println("0.05 + 0.01 = " + (0.05 + 0.01f) + "  => " + add(10, 0.05, 0.01));
        System.out.println("1.0 - 0.42 = " + (1.0f - 0.42f) + "  => " + sub(10, 1.0, 0.42));
        System.out.println("4.015 * 100 = " + (4.015 * 100) + "  => " + mul(10, 4.015, 100d));
        System.out.println("123.3 / 100 = " + (123.3 / 100) + "  => " + div(10, 123.3, 100d));
    }

    public static Double add(int scale, Double ... args) {
        BigDecimal sum = new BigDecimal("0");
        for (double arg : args) {
            sum = sum.add(new BigDecimal(String.valueOf(arg)));
        }
        return sum.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    public static Double sub(int scale, Double v1, Double v2) {
        BigDecimal b1 = new BigDecimal(String.valueOf(v1));
        BigDecimal b2 = new BigDecimal(String.valueOf(v2));
        return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    public static Double mul(int scale, Double ... args) {
        BigDecimal sum = new BigDecimal("1");
        for (double arg : args) {
            sum = sum.multiply(new BigDecimal(String.valueOf(arg)));
        }
        return sum.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    public static Double div(int scale, Double v1, Double v2) {
        BigDecimal b1 = new BigDecimal(String.valueOf(v1));
        BigDecimal b2 = new BigDecimal(String.valueOf(v2));
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
    
    

输出结果如下:

0.05 + 0.01 = 0.059999999776482585  => 0.06
1.0 - 0.42 = 0.58000004  => 0.58
4.015 * 100 = 401.49999999999994  => 401.5
123.3 / 100 = 1.2329999999999999  => 1.233

可以看到BigDecimal计算的结果是正确的,BigDecimal计算的原理就是将浮点数进行放大,放大成整数进行计算,几个主要变量如下:

//有多少位小数(即小数点后有多少位),也就是放大倍数
int scale; 
//一共有多少位数字
int precision; 
//字符串放大后,转为的long值,只有当传的字符串长度小于18时才使用该值。
long intCompact; 
//当传的字符串长度大于等于18时才使用BigInteger表示数字,此时intCompact为Long.MAX_VALUE
BigInteger intVal; 

微信扫一扫关注该公众号

欢迎关注我的微信公众号

你可能感兴趣的:(JVM)