精度丢失
在工作中经常会遇到数值精度问题,比如说使用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定义了一种浮点类型数值在计算机内部的存储方式。
无论是单精度还是双精度在存储中都分为三个部分:
- 符号位(sign) : 0表示正,1表示负。占1bit;
- 指数位(biased exponent):首先exponent表示该域用于表示指数,也就是数值可表示数值范围,而biased则表示它采用偏移的编码方式。那么什么是采用偏移的编码方式呢?也就是位模式中没有设立sign-bit,而是通过设置一个中间值作为0,小于该中间值则为负数,大于改中间值则为正数。IEEE 754中规定bias = 2^e-1 - 1,e为Biased-exponent所占位数;
- 尾数部分(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.