Java之道系列:BigDecimal如何解决浮点数精度问题

如题,今天我们来看下java.math.BigDecimal是如何解决的,在那之前当然得先了解下浮点数精度问题是什么问题了。下面我们先从IEEE 754说起。

IEEE 754

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

下面我们就以双精度,也就是double类型,为例来看看浮点数的格式。

sign exponent fraction

1位 11位 52位

63 62-52实际的指数大小+1023 51-0

下面看个栗子,直接输出double类型的二进制表示,

(String[] args) {printBits(3.5);}(double d) {System.out.println(“##”+d);long l = Double.doubleToLongBits(d);String bits = Long.toBinaryString(l);int len = bits.length();System.out.println(bits+”#”+len);if(len == 64) {System.out.println(“[63]”+bits.charAt(0));System.out.println(“[62-52]”+bits.substring(1,12));System.out.println(“[51-0]”+bits.substring(12, 64));} else {System.out.println(“[63]0”);System.out.println(“[62-52]”+ pad(bits.substring(0, len – 52)));System.out.println(“[51-0]”+bits.substring(len-52, len));}}private static String pad(String exp) {int len = exp.length();if(len == 11) {return exp;} else {StringBuilder sb = new StringBuilder();for (int i = 11-len; i > 0; i–) {sb.append(“0”);}sb.append(exp);return sb.toString();}}##3.5100000000001100000000000000000000000000000000000000000000000000#63[63]0[62-52]10000000000[51-0]1100000000000000000000000000000000000000000000000000

指数大小为10000000000B-1023=1,尾数为1.11B,所以实际数值大小为11.1B=3.5,妥妥的。 有一点需要注意的是上述格式为,所以尾数的整数部分为1,而当时,尾数的整数部分是为0的。

0.1 Orz

上面我们使用的浮点数3.5刚好可以准确的用二进制来表示,,但并不是所有的小数都可以用二进制来表示,例如,0.1。

(String[] args) {printBits(0.1);}##0.111111110111001100110011001100110011001100110011001100110011010#62[63]0[62-52]01111111011[51-0]1001100110011001100110011001100110011001100110011010

0.1无法表示成+… 这样的形式,尾数部分后面应该是1100一直循环下去(纯属猜测,不过这个应该也是可以证明的),但是由于计算机无法表示这样的无限循环,所以就需要截断,这就是浮点数的精度问题。精度问题会带来一些unexpected的问题,例如0.1 + 0.1 + 0.1 == 0.3将会返回false,

(String[] args) {System.out.println(0.1 + 0.1 == 0.2); // trueSystem.out.println(0.1 + 0.1 + 0.1 == 0.3); // false}

那么BigDecimal又是如何解决这个问题的?

BigDecimal

BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数,

(String[] args) {BigDecimal bd = new BigDecimal(“100.001”);System.out.println(bd.scale());System.out.println(bd.unscaledValue());}

输出,

3100001

也就是100.001 = 100001 * 0.1^3。这种表示方式下,避免了小数的出现,当然也就不会有精度问题了。十进制,也就是整数部分使用了BigInteger来表示,小数点位置只需要一个整数scale来表示就OK了。 当使用BigDecimal来进行运算时,也就可以分解成两部分,BigInteger间的运算,以及小数点位置scale的更新,下面先看下运算过程中scale的更新。

scale

加法运算时,根据下面的公式scale更新为两个BigDecimal中较大的那个scale即可。

X*) * ) *

相应的代码如下,

/*** Returns a {@code BigDecimal} whose value is {@code (this +* augend)}, and whose scale is {@code max(this.scale(),* augend.scale())}.** @param augend value to be added to this {@code BigDecimal}.* @return {@code this + augend}*/public BigDecimal add(BigDecimal augend) {long xs = this.intCompact;long ys = augend.intCompact;BigInteger fst = (xs != INFLATED) ? null : this.intVal;BigInteger snd = (ys != INFLATED) ? null : augend.intVal;int rscale = this.scale;long sdiff = (long)rscale – augend.scale;if (sdiff != 0) {if (sdiff < 0) {int raise = checkScale(-sdiff);rscale = augend.scale;if (xs == INFLATED ||(xs = longMultiplyPowerTen(xs, raise)) == INFLATED)fst = bigMultiplyPowerTen(raise);} else {int raise = augend.checkScale(sdiff);if (ys == INFLATED ||(ys = longMultiplyPowerTen(ys, raise)) == INFLATED)snd = augend.bigMultiplyPowerTen(raise);}}if (xs != INFLATED && ys != INFLATED) {long sum = xs + ys;( (((sum ^ xs) & (sum ^ ys))) >= 0L) // not overflowedreturn BigDecimal.valueOf(sum, rscale);}if (fst == null)fst = BigInteger.valueOf(xs);if (snd == null)snd = BigInteger.valueOf(ys);BigInteger sum = fst.add(snd);return (fst.signum == snd.signum) ?new BigDecimal(sum, INFLATED, rscale, 0) :new BigDecimal(sum, rscale);}

乘法运算根据下面的公式也可以确定scale更新为两个scale之和。

X*

偶尔被惊鸿一瞥的美丽吸引;或者走进一条深沉深沉的巷道,

Java之道系列:BigDecimal如何解决浮点数精度问题

相关文章:

你感兴趣的文章:

标签云: