BigDecimal的原理

BigDecimal的使用

通常我们需要精确计算的时候会选择java.math.BigDecimal来进行计算。

介绍

Java中基础的小数的数据类型为float和double,他们在计算机底层是通过二进制分别表示指数和尾数来进行存储时,故造成存储时失去准确性。比如输入下面代码:

System.out.println(0.8-0.1);

结果为0.7000000000001。而BigDecimal使用**十进制(BigInteger)+ 小数点位置(scale)**来表示,更准确

奇奇怪怪的对不对,可以参见Java的BigDecimal如何解决浮点数精度问题这篇博客,比较详细解释浮点数的存储,以及如何转换成BigDecimal,写的很好。当然,如果你只是想要使用BigDecimal,可以记住结论然后我们开始看下一部分。

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

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

在这里插入图片描述

signexponentfraction
1位11位52位
6362-52 实际的指数大小+102351-0
public static void main(String[] args) {
    printBits(3.5);
}

private static void printBits(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.5
100000000001100000000000000000000000000000000000000000000000000#63
[63]0
[62-52]10000000000
[51-0]1100000000000000000000000000000000000000000000000000

0.1 orz

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

public static void main(String[] args) {
    printBits(0.1);
}
##0.1
11111110111001100110011001100110011001100110011001100110011010#62
[63]0
[62-52]01111111011
[51-0]1001100110011001100110011001100110011001100110011010

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

public static void main(String[] args) {
    System.out.println(0.1 + 0.1 == 0.2); // true
    System.out.println(0.1 + 0.1 + 0.1 == 0.3); // false
}

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

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

public static void main(String[] args) {
    BigDecimal bd = new BigDecimal("100.001");
    System.out.println(bd.scale());
    System.out.println(bd.unscaledValue());
}
3
100001

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

scale

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

X ∗ 0. 1 n + Y ∗ 0. 1 m = = X ∗ 0. 1 n + ( Y ∗ 0. 1 m − n ) = = ( X + Y ∗ 0. 1 m − n ) ∗ 0. 1 n ,其中 n > m X*0.1^n+Y*0.1^m==X*0.1^n+(Y*0. 1^{m− n})==(X+Y*0.1^{m-n})*0.1^n,其中n > m X0.1n+Y0.1m==X0.1n+(Y0.1mn)==(X+Y0.1mn)0.1n,其中n>m

源码如下:

    /**
     * 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;
            // See "Hacker's Delight" section 2-12 for explanation of
            // the overflow test.
            if ( (((sum ^ xs) & (sum ^ ys))) >= 0L) // not overflowed
                return 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 ∗ 0. 1 n ∗ Y ∗ 0. 1 m = = ( X ∗ Y ) 0. 1 n + m X*0.1^n*Y*0.1^m==(X*Y)0.1^{n+m} X0.1nY0.1m==(XY)0.1n+m

/**
 * Returns a {@code BigDecimal} whose value is <tt>(this &times;
 * multiplicand)</tt>, and whose scale is {@code (this.scale() +
 * multiplicand.scale())}.
 *
 * @param  multiplicand value to be multiplied by this {@code BigDecimal}.
 * @return {@code this * multiplicand}
 */
public BigDecimal multiply(BigDecimal multiplicand) {
    long x = this.intCompact;
    long y = multiplicand.intCompact;
    int productScale = checkScale((long)scale + multiplicand.scale);

    // Might be able to do a more clever check incorporating the
    // inflated check into the overflow computation.
    if (x != INFLATED && y != INFLATED) {
        /*
         * If the product is not an overflowed value, continue
         * to use the compact representation.  if either of x or y
         * is INFLATED, the product should also be regarded as
         * an overflow. Before using the overflow test suggested in
         * "Hacker's Delight" section 2-12, we perform quick checks
         * using the precision information to see whether the overflow
         * would occur since division is expensive on most CPUs.
         */
        long product = x * y;
        long prec = this.precision() + multiplicand.precision();
        if (prec < 19 || (prec < 21 && (y == 0 || product / y == x)))
            return BigDecimal.valueOf(product, productScale);
        return new BigDecimal(BigInteger.valueOf(x).multiply(y), INFLATED,
                              productScale, 0);
    }
    BigInteger rb;
    if (x == INFLATED && y == INFLATED)
        rb = this.intVal.multiply(multiplicand.intVal);
    else if (x != INFLATED)
        rb = multiplicand.intVal.multiply(x);
    else
        rb = this.intVal.multiply(y);
    return new BigDecimal(rb, INFLATED, productScale, 0);
}

BigInteger

BigInteger可以表示任意精度的整数。当你使用long类型进行运算,可能会产生溢出时就要考虑使用BigInteger了。BigDecimal就使用了BigInteger作为backend。
那么BigInteger是如何做到可以表示任意精度的整数的?答案是使用数组来表示,看下面这个栗子就很直观了,

public static void main(String[] args) {
    byte[] mag = {
            2, 1 // 10 00000001 == 513
    };
    System.out.println(new BigInteger(mag));
}

通过byte[]来当作底层的二进制表示,例如栗子中的[2, 1],也就是[00000010B, 00000001B],就是表示二进制的10 00000001B这个数,也就是513了。
BigInteger内部会将这个byte[]转换成int[]保存,代码在stripLeadingZeroBytes方法,

/**
 * Translates a byte array containing the two's-complement binary
 * representation of a BigInteger into a BigInteger.  The input array is
 * assumed to be in <i>big-endian</i> byte-order: the most significant
 * byte is in the zeroth element.
 *
 * @param  val big-endian two's-complement binary representation of
 *         BigInteger.
 * @throws NumberFormatException {@code val} is zero bytes long.
 */
public BigInteger(byte[] val) {
    if (val.length == 0)
        throw new NumberFormatException("Zero length BigInteger");

    if (val[0] < 0) {
        mag = makePositive(val);
        signum = -1;
    } else {
        mag = stripLeadingZeroBytes(val);
        signum = (mag.length == 0 ? 0 : 1);
    }
}
/**
 * Returns a copy of the input array stripped of any leading zero bytes.
 */
private static int[] stripLeadingZeroBytes(byte a[]) {
    int byteLength = a.length;
    int keep;

    // Find first nonzero byte
    for (keep = 0; keep < byteLength && a[keep]==0; keep++)
        ;

    // Allocate new array and copy relevant part of input array
    int intLength = ((byteLength - keep) + 3) >>> 2;
    int[] result = new int[intLength];
    int b = byteLength - 1;
    for (int i = intLength-1; i >= 0; i--) {
        result[i] = a[b--] & 0xff;
        int bytesRemaining = b - keep + 1;
        int bytesToTransfer = Math.min(3, bytesRemaining);
        for (int j=8; j <= (bytesToTransfer << 3); j += 8)
            result[i] |= ((a[b--] & 0xff) << j);
    }
    return result;
}

上面也可以看到这个byte[]应该是big-endian two's-complement binary representation。
那么为什么构造函数不直接让我们扔一个int[]进去就得了呢,还要这么转换一下?答案是因为Java的整数都是有符号整数,举个栗子,int类型没办法表示232-1
也就是32位上全都是1这个数的,这时候用byte[]得这么写,(byte)255,(byte)255,(byte)255,(byte)255,这样才能表示32个1。

BigDecimal方法

一下内容均翻译理解自官方文档:https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html,其中的参数名为scale的都是指小数点位数。

构造函数

BigDecimal的构造函数多达16个,这里就不都翻译了,只说常用方法和注意事项,以下皆是如此。常见的构造方法为,很多人喜欢通过double构造,这样无法保证精度,所以最好使用字符串的形式进行构造。究其原因,BigDecimal在通过double构造时,会将double变量转换成IEEE 754类型,精度就已经损失了。

BigDecimal(String val); 
BigDecimal BigDecimal(double d); //不允许使用,精度不能保证

除此之外,构造函数的参数还可以是char[]、int、long、BigInteger类型的变量,其中char[]还可为其指定范围实例如下,输出bD结果为2345。

String str = "1234567";
char[] chars = str.toCharArray();
BigDecimal bD = new BigDecimal(chars, 1,4);

赋值及实例

static BigDecimal valueOf(double val)
static BigDecimal valueOf(long val)
static BigDecimal valueOf(long unscaledVal, int scale)//指定小数点位数
BigDecimal bD = BigDecimal.valueOf(12345679,2);//结果为123456.79

基本数学操作

加减乘除等,返回值都是BigDecimal,如下表,再次说明scale表示小数点位数。roundingMode表示四舍五入的方式,可以直接写数字或者RoundingMode枚举类型,舍入模式请参考:BigDecimal使用

方法作用
abs()绝对值,scale不变
add(BigDecimal augend)加,scale为augend和原值scale的较大值
subtract(BigDecimal augend)减,scale为augend和原值scale的较大值
multiply(BigDecimal multiplicand)乘,scale为augend和原值scale的和
divide(BigDecimal divisor)除,原值/divisor,如果不能除尽会抛出异常,scale与原值一致
divide(BigDecimal divisor, int roundingMode)除,指定舍入方式,scale与原值一致
divide(BigDecimal divisor, int scale, int roundingMode)除,指定舍入方式和scale
remainder(BigDecimal divisor)取余,scale与原值一致
divideAndRemainder(BigDecimal divisor)除与取余,返回BigDecimal数组,两个元素,0为商,1为余数,scale与原值一致
divideToIntegralValue(BigDecimal divisor)除,只保留整数部分,但scale仍与原值一致
max(BigDecimal val)较大值,返回原值与val中的较大值,与结果的scale一致
min(BigDecimal val)较小值,与结果的scale一致
movePointLeft(int n)小数点左移,scale为原值scale+n
movePointRight(int n)小数点右移,scale为原值scale+n
negate()取反,scale不变
pow(int n)幂,原值^n,原值的n次幂
scaleByPowerOfTen(int n)相当于小数点右移n位,原值*10^n
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值