浮点类型精度丢失和BigDecimal

精度丢失

在工作中经常会遇到数值精度问题,比如说使用float或者double的时候,可能会有精度丢失问题,下面来总结一下吧。

为了引出问题,先看一个例子(Java代码):

    public static void main(String[] args) {
        float f = 2.25f;
        double d = (double) f;
        System.out.println(d);

        f = 2.2f;
        d = (double) f;
        System.out.println(d);
    }

结果如下:

2.25
2.200000047683716

问题出现了,单精度类型的2.2在转换为双精度类型后,2.2变成了2.200000047683716。

下面分析下原因。
对于浮点类型,Oracle的JVM规范(jdk8)是这样定义的:

浮点类型是 float和double,它们在概念上与32位单精度和64位双精度格式的IEEE 754值以及在IEEE二进制浮点算术标准(ANSI / IEEE Std。 754-1985,纽约)。

引文可参考:浮点类型,值集和值
那么什么是IEEE二进制浮点算术标准呢?

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

官方文档:754-2008-浮点算法的IEEE标准

简单来说,就是IEEE 754定义了一种浮点类型数值在计算机内部的存储方式。
无论是单精度还是双精度在存储中都分为三个部分:

  1. 符号位(sign) : 0表示正,1表示负。占1bit;
  2. 指数位(biased exponent):首先exponent表示该域用于表示指数,也就是数值可表示数值范围,而biased则表示它采用偏移的编码方式。那么什么是采用偏移的编码方式呢?也就是位模式中没有设立sign-bit,而是通过设置一个中间值作为0,小于该中间值则为负数,大于改中间值则为正数。IEEE 754中规定bias = 2^e-1 - 1,e为Biased-exponent所占位数;
  3. 尾数部分(trailing significand field):尾部有效位字段,也就是数值可表示的精度。

如下图所示:
在这里插入图片描述
对於单精度类型(32bit)和双精度类型(64bit),其不同之处在于指数位,单精度为8位指数位,而双精度为11位。其实里面有个公式,可以对不同精度的数值算出不同的指数位,这里不展开了。
如图所示:
在这里插入图片描述
再回到上面那个问题,2.25的单精度和双精度分别是:

单精度:0 1000 0001 001 0000 0000 0000 0000 0000
双精度:0 100 0000 0001 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

这样2.25在进行强制转换的时候,数值是不会变的,而我们再看看2.2呢。
2.2的小数部分在转换为二进制的时候,是乘不尽的,得到的二进制是一个无限循环的排列 00110011001100110011…,如果是单精度的,那么就是:

单精度:0 1000 0001 001 1001 1001 1001 1001 1001

那么以这样的存储方式,当它再转换为十进制的时候,就不再是2.2了。
双精度也是如此。
这就是精度的丢失。

BigDecimal

BigDecimal为什么就能避免精度的丢失问题呢?
首先,BigDecimal的存储方式并不是像浮点类型一样在计算机中直接存储的,而是Java通过封装了一系列的基本类型来实现的,它会把一个浮点类型的数值拆分进行分别存储。
BigDecimal里面有下面四个主要的字段:

   private final BigInteger intVal;
   private final int scale;
   private transient int precision;
   private final transient long intCompact;
  • intVal:有效数字(去掉前缀0和小数点)的数值,比如-092233720368.54775807,intVal就是-9223372036854775807。
  • scale:比例尺度,也就是小数位数。比如说2.567,那么scale就是3;如果没有小数位,那么scale就是0.
  • precision:精度,也就是有效数字的位数。比如12.567,那么precision就是5,即使输入“-0012.567”,precision仍然是5。
  • intCompact:由于intVal的类型是BigInteger,需要占用更多的内存空间,所以增加了long类型的intCompact字段。如果此BigDecimal的有效数字的绝对值小于或等于Long.MAX_VALUE,该值可以存储在此字段中,并用于计算。

这样的存储组合方式,就可以在计算的时候直接使用intVal或者intCompact进行计算了,而整数直接的计算是不会产生精度丢失问题的。

如果有小数的话,只需要对比scale,然后补全scale较小的那个值,进行小数点对齐之后再计算就可以了。

比如说9.53和2.1进行相加的时候,实际上就是scale=2的条件下的953+210=1163,因为scale=2,所以表示的真实数值就是11.63.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章