BigDecimal的使用和一些坑

  • Post author:
  • Post category:其他


在金融领域进行业务计算时,因为要保证精度的准确,通常不建议使用float和double进行数学计算。

float和double类型主要是为了科学计算和工程计算而设计的。他们执行二进制浮点运算,这是为了在广泛的数字范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以我们不应该用于精确计算的场合。float和double类型尤其不适合用于货币运算,因为要让一个float或double精确的表示0.1或者10的任何其他负数次方值是不可能的(其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10)



为什么浮点型运算会造成精度丢失

首先我们要搞清楚下面两个问题:

 (1) 十进制整数如何转化为二进制数

       算法很简单。举个例子,11表示成二进制数:

                 11/2=5 余   1

                   5/2=2   余   1

                   2/2=1   余   0

                   1/2=0   余   1

                      0结束         11二进制表示为(从下往上):1011

      这里提一点:只要遇到除以后的结果为0了就结束了,大家想一想,所有的整数除以2是不是一定能够最终得到0。换句话说,所有的整数转变为二进制数的算法会不会无限循环下去呢?绝对不会,整数永远可以用二进制精确表示 ,但小数就不一定了。

  (2) 十进制小数如何转化为二进制数

       算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

                 0.9*2=1.8   取整数部分 1

                 0.8(1.8的小数部分)*2=1.6    取整数部分 1

                 0.6*2=1.2   取整数部分 1

                 0.2*2=0.4   取整数部分 0

                 0.4*2=0.8   取整数部分 0

                 0.8*2=1.6 取整数部分 1

                 0.6*2=1.2   取整数部分 0

                          .........      0.9二进制表示为(从上往下): 1100100100100......

       注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

所以在进行精确计算时,java中推荐使用BigDecimal,同样在BigDecimal的使用中也有一些需要注意的地方,如果注意就可能导致同样的精度损失问题。



1.在bigDecimal初始化时,不要使用double或者float类型的值传入构造器

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。

对比可知,test2仍然会出现精度问题,而在创建BigDecimal对象时,参数为字符串就不会出现精度问题

所以总结如下


```java
//使用String构造
BigDecimal b1 = new BigDecimal("0.1");
//或者是:
BigDecimal b1 = BigDecimal.valueOf(0.1);
//点开valueOf的源码,可以看到在源码中也是用new BigDecimal(String);返回一个BigDecimal对象的
//源码如下:
public static BigDecimal valueOf(double val) {
    // Reminder: a zero double returns '0.0', so we cannot fastpath
    // to use the constant ZERO.  This might be important enough to
    // justify a factory approach, a cache, or a few private
    // constants, later.
    return new BigDecimal(Double.toString(val));
}



2.在进行BigDecimal数值比较时不要使用equals进行比较

使用equals进行比较会比较值的大小和精度的大小,即0.00和0.000是不相等的,要使用compareTo()来进行比较。



BigDecimal使用中的报错

Division is undefined错误

  1. 此错误仅在使用BigDecimal做除法时,且0/0的情况下才会提示。

    x/0时,仅提示Division by zero。
  2. BigDecimal判断一个值是否为0时,不能使用equals,因为equals会比较值的大小和精度的大小,即0.00 和 0.000是不同的。

    需要使用 x.compareTo(BigDecimal.ZERO) == 0 来判断。
  3. 且使用divide做除法时,标准的形式为 x.divide(y, scale,rm)

    如果不指明scale的值,会默认使用 x.scale – y.scale的值替代,如果此时值为负数,则会报错 Division undefined。

    且不指定scale 和 rm四舍五入方式,如果遇到 1/3 这种除不尽的情况,会报如下错误:

    Non-terminating decimal expansion; no exact representable decimal result.



BigDecimalUtil工具类 –解决精度问题


```java
public class BigDecimalUtil {
    //防止工具类在外部实例化
    private BigDecimalUtil(){
    }
    public static BigDecimal add(double v1, double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2);
    }
    public static BigDecimal sub(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2);
    }
    public static BigDecimal mul(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2);
    }
    public static BigDecimal div(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.divide(b2,2,BigDecimal.ROUND_HALF_UP);//四舍五入,保留2位小数
    }
}



BigDecimal的开平方方法,使用牛顿迭代法

在这里插入图片描述

在浮点数进行计算时可以使用Math.sqrt()方法进行开方运算,在使用BigDecimal时没有提供开方方法,需要我们自己实现。

牛顿迭代法

牛顿迭代法,又称切线法,由牛顿首次提出。其算法详细过程如下图所示:

以方程 x2=nx2=n 为例,令 f(x)=x2−nf(x)=x2−n,也就是相当于求解 f(x)=0f(x)=0 的解。首先随便找一个初始值 x0x0,如果 x0x0 不是解,做一个经过 (x0,f(x0))(x0,f(x0)) 这个点的切线,与轴的交点为 x1x1。同理,如果 x1x1 不是解,做一个经过(x1,f(x1))(x1,f(x1)) 这个点的切线,与轴的交点为 x2x2。 以此类推……以这样的方式得到的会无限趋近于 f(x)=0f(x)=0 的解。

判断是否是的解有两种方法:1. 直接计算的值判断f(x)是否为0;2. 判断前后两个解和是否无限接近。

经过这个点 (xi,f(xi))(xi,f(xi)) 的切线方程为 f(x)=f(xi)+f′(xi)(x−xi)f(x)=f(xi)+f′(xi)(x−xi),

其中,f′(xi)f′(xi) 为 f(xi)f(xi) 的导数,本题中导数为 2x2x。令切线方程等于0(纵轴截距取0),即可求出:

xi+1=xi−f(xi)f′(xi)

xi+1=xi−f(xi)f′(xi)

带入 f(x)=x2−nf(x)=x2−n,继续化简:

xi+1=xi−x2i−n2xi=xi−xi2+n2xi=xi2+n2xi

xi+1=xi−xi2−n2xi=xi−xi2+n2xi=xi2+n2xi

基于上述迭代公式,可以给出了一个求平方根的算法。事实上,这也的确是很多语言中内置的开平方函数的实现方法。牛顿迭代法也同样适用于求解其他多次方程的解。

private BigDecimal sqrt(BigDecimal value, int scale) {
    if(value.compareTo(BigDecimal.ZERO)==0){
        return new BigDecimal(0);
    }
    BigDecimal num2 = BigDecimal.valueOf(2);
    int precision = 100;
    MathContext mc = new MathContext(precision, RoundingMode.HALF_UP);
    BigDecimal deviation = value;
    int cnt = 0;
    while (cnt < precision) {
        deviation = (deviation.add(value.divide(deviation, 100,BigDecimal.ROUND_HALF_UP))).divide(num2,100,BigDecimal.ROUND_HALF_UP);
        cnt++;
    }
    deviation = deviation.setScale(scale, BigDecimal.ROUND_HALF_UP);
    return deviation;
}



版权声明:本文为zzMystery原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。