浮点数 为什么不能精确

一、float、double 在计算机中的存储方式:

我们知道计算机存储数据都是以二进制方式进行存储的,那么浮点数是怎么存储的呢?
比如二进制数101.11 就等于 1 * 2^2 +0 *2^1 + 1*2^0 + 1*2^-1 + 1*2^-2  = 4+0+1+1/2+1/4 = 5.75
下面的图展示了一个二进制小数的表达形式:
float-double-store
计算机对float与double类型都是将十进制数转换为二进制数再小数部分与指数部分分开存储,两者均使用自己独立的符号位,且由于二进制只为1或0,而一个二进制数必然可以设置成为1.XXX,其中X为0或1,所以该数可以默认为1.XXX,使用指数为控制整数部分,整数后面的自然就转换为了小数部分。
根据国际标准IEEE 754,任意一个二进制浮点数V可以表示成下面的形式:
IEEE-v-sme
 (1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
 (2)M表示有效数字,大于等于1,小于2。
 (3)2^E表示指数位。

float的存储方式:

float-store-format

double的存储方式:

double-store-format
位数部分是被整数部分和小数部分共同占有的,当一个数字整数部分非常大时就会占用更多的位数,留给小数部分的位数就会变少,从而可能导致精读的损失。

二、分析浮点数“132541.35”精读丢失的原因:

十进制整数部分转二进制(132541):

整数部分转二进制是将整数部分进行迭代除2取余,直到商为0为止,将每次迭代之后的余数连接起来就行了。
132541-35-integer

十进制小数部分转二进制(0.35):

十进制小数转成二进制小数,是需要对小数部分进行迭代乘2,每次迭代整数部分如果是1,则取小数部分进行下一次迭代,直到小数部分为0为止。
 132541-35-decimal-part.png
如上图所示,当得到0.4的小数部分之后,之后的每次迭代乘以2之后永远也没有办法得到整数1,就是以“0110”无限循环,所以0.35 不能用有限位的二进制表示,这就像1/3是无法用有限位的十进制表示是一样的道理,随着0.3333的尾数精度增加,只能无限趋近于1/3的真实值而不可能相等。
完整的“132541.35”二进制表示:
132541-35-all-part
float是用32位存储的,前一位是符号位,中间8位为指数位,最后23位为尾数位,那么“132541.35”在计算机中存储结构:
float-13241-35-mem-store
这个例子中,我们就知道了为什么浮点数在一些场景下是不可能做到精确的了,跟其在计算机中的存储结构有关,我们能做的是尽可能接近准确值。

三、提高精度:

目前我们在处理价格数据的时候解决精度主要采用两种方案:
  • 使用BigDecimal
  • 使用long存储(存储分或者厘)

BigDecimal:

在《Effective Java》中48章提到,float和double能用來做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal。
那为什么进度计算的时候使用BigDecimal,BigDecimal是如何保证精读的呢?
BigDecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。因此,BigDecimal表示的数值是(unscaledValue × 10-scale),在小数位转换为二进制后出现无限循环的场景下,BigDecimal做的也只是尽量接近真实值,而不能等于真实值。
测试BigDecimal中的scale 和 unscaledValue:
        BigDecimal bigDecimal = new BigDecimal(100.11d);
        System.out.println("scale = " + bigDecimal.scale());
        System.out.println("unscaledValue = " + bigDecimal.unscaledValue());
        System.out.println("------");
        bigDecimal = new BigDecimal("100.11");
        System.out.println("scale = " + bigDecimal.scale());
        System.out.println("unscaledValue = " + bigDecimal.unscaledValue());
输出结果为:
scale = 46
unscaledValue = 1001099999999999994315658113919198513031005859375
------
scale = 2
unscaledValue = 10011
new BigDecimal(double val)  或者  new BigDecimal(String val) 如何选择?
new BigDecimal(double val)方法上的JavaDoc注解如下:
Notes:
The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.
The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal(“0.1”) creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.
原來如果需要精确计算,非要用String來构造BigDecimal不可,推荐使用 new BigDecimal(String val) ;其次,BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。

使用long存储:

在数据库存储层面存储分或者厘,每次在取出计算或者展示的时候除以100,存储到数据库的时候乘以100,这对业务处理上有一定的侵入性,而且容易出错,多一次或者少一次100的转换都可能给业务带来巨大的损失,所以建议定义一个Money类,专门用户处理与钱相关的业务逻辑,相当于对数据库层的数据做了一层封装,比如订单系统中的Money.java 类。
一般我们存储在数据库中的long型数值为金钱的分,但是当一些对精度要求比较高的场景下,可以存储厘,特别是银行体系中,那美元来做换算时,如果商品数量非常大的情况下,误差就会被放大。

参考:

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s