商业计算工具——Bigdecimal详解

前言

对于浮点数的四则运算,我们知道可能会出现精度上的误差,对于不需要准确计算精度的计算我们可以使用doublefloat来操作,但是涉及到金额交易(计算)等场景的时候,就需要我们特别注意结果的精度问题,这个时候,我们就可以采用Bigdecimal类来进行高精度的四则运算了。本篇文章将对Bigdecimal的使用场景和具体的使用方法进行讲解,希望对读者能够有所帮助。

最前面的小例子

我们对下面的代码进行计算

@Test
    public void mathTest(){
        double b1 = 0.3;
        double b2 = 4.2;
        double b3 = 0.1;
        double bb = 0.2;
        double b4 = b2 - b1;
        double b5 = b2 / b1;
        double b6 = bb + b3;
        double b7 = b3 * bb;
        System.out.println("b4 = " + b4);
        System.out.println("b5 = " + b5);
        System.out.println("b6 = " + b6);
        System.out.println("b7 = " + b7);
    }

看上去答案很简单,但我们看一下打印结果:


我们发现,上述浮点数相加的结果并不是原先预想的值,原因在于我们的计算机是二进制的。
浮点数没有办法是用二进制进行精确表示。我们的CPU表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。如:2.4的二进制表示并非就是精确的2.4。反而最为接近的二进制表示是 2.3999999999999999。浮点数的值实际上是由一个特定的数学公式计算得到的。(后面有空会再出一篇文章来讲这个)
所以很多涉及商业计算的功能的话,最好使用Bigdecimal来统一进行计算。

Bigdecimal的使用详解

(一)常用构造函数
  • public BigDecimal(double val) *尽量不要使用
    创建一个具有参数所指定双精度数值的对象
  • public BigDecimal(String val)
    创建一个具有参数所指定双精度数值的对象
  • public BigDecimal(long val)
    创建一个具有参数所指定双精度数值的对象
  • public BigDecimal(int val)
    创建一个具有参数所指定双精度数值的对象

使用构造函数的注意事项
我们看一下下面这个例子

    @Test
    public void DecimalTest(){
        BigDecimal b1 = new BigDecimal(0.1);
        BigDecimal b2 = new BigDecimal("0.1");
        System.out.println("b1 = " + b1);
        System.out.println("b2 = " + b2);
    }


我们看到,虽然这里我们使用了入参为double类型的构造函数,但是当打印出来后我们看到结果依然不是我们预期的结果。所以我们使用Bigdecimal的时候最好用String类型的构造函数,如果原入参是double的话,可以通过Bigdecimal的静态方法valueOf或者用String.valueOf来进行定义。

    @Test
    public void DecimalTest(){
        BigDecimal b1 = new BigDecimal(String.valueOf(0.1));
        BigDecimal b2 = BigDecimal.valueOf(0.1);
        System.out.println("b1 = " + b1);
        System.out.println("b2 = " + b2);
    }
(二)Bigdecimal的常见运算、
1. 四则运算
@Test
    public void arithmeticTest(){
        BigDecimal b1 = BigDecimal.valueOf(0.3);
        BigDecimal b2 = BigDecimal.valueOf(4.2);
        BigDecimal b3 = BigDecimal.valueOf(0.1);
        BigDecimal b4 = BigDecimal.valueOf(0.2);
        System.out.println("b2 - b1 = " + b2.subtract(b1));
        System.out.println("b3 + b4 = " + b3.add(b4));
        System.out.println("b3 * b4 = " + b3.multiply(b4));
        System.out.println("b2 / b1 = " + b2.divide(b1));
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1)));
    }


我们可以看到,使用了Bigdecimal后,我们可以顺利地解决之前出现的精度问题了,但是在计算最后一个式子10.1/3.1的时候,却抛出了一个异常。这就引出了Bigdecimal的除法问题。

  • Bigdecimal的除法
    当使用该类进行除法运算时,如果出现无法整除的情况,那么程序就会抛出ava.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.的错误。这个时候应该怎么解决呢?
    答案是使用devide的重载方法:

public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
    第1个参数表示除数
    第2个参数表示保留的小数位数
    第3个参数表示四舍五入的模式

具体的模式如下:

ROUND_CEILING     向正无穷方向舍入
ROUND_DOWN        向零方向舍入
ROUND_FLOOR       向负无穷方向舍入
ROUND_HALF_DOWN    向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入, 例如1.55 保留一位小数结果为1.5
ROUND_HALF_EVEN    向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_HALF_UP     向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6
ROUND_UNNECESSARY    计算结果是精确的,不需要舍入模式
ROUND_UP            向远离0的方向舍入

注意!!在java9之前,舍入模式是放在Bigdecimal的成员变量里面的,但是从Java9之后,推出了RoundingMode枚举类来替代原先的做法。如果使用还使用原先的做法的话,会提醒已过时,建议开发人员后续还是使用最新的规范来开发。

    @Test
    public void devideTest(){
        System.out.println("10.1 / 3.1 = " + (10.1 / 3.1));
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.HALF_UP)); // 四舍五入
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.UP));  // 向上取整
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.CEILING)); // 向上取整
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.DOWN)); // 向下取整
        System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.FLOOR));// 向下取整
    }
2. Bigdecimal大小比较

对于Bigdecimal对象大小的比较比较简单,直接使用compareTo

    @Test
    public void compareTest(){
        BigDecimal b1 = BigDecimal.valueOf(1.5);
        BigDecimal b2 = BigDecimal.valueOf(2.5);
        BigDecimal b3 = BigDecimal.valueOf(2.5);
        System.out.println("b1 compare to b2 : " + b1.compareTo(b2));
        System.out.println("b2 compare to b1 : " + b2.compareTo(b1));
        System.out.println("b2 compare to b3 : " + b2.compareTo(b3));
    }

打印结果如下:


我们可以看到,结果为-1表示小于,结果为1表示大于,结果为0表示等于

(三)Bigdecimal的格式化

我们可以使用NumberFormat来对bigdecimal类型的数据进行格式化。下面举例百分比和货币格式的格式化方式。

    @Test
    public void formatTest(){
        // 创建货币格式化对象
        NumberFormat currencyInstance = NumberFormat.getCurrencyInstance();
        // 创建百分比格式化对象
        NumberFormat percentInstance = NumberFormat.getPercentInstance();
        // 设置百分比最大的小数点位数为3位
        percentInstance.setMaximumFractionDigits(3);
        BigDecimal capital = BigDecimal.valueOf(10000.00);
        BigDecimal rate = BigDecimal.valueOf(0.15123);
        BigDecimal interest = capital.multiply(rate);
        System.out.println("本金为: " + currencyInstance.format(capital));
        System.out.println("利率为: " + percentInstance.format(rate));
        System.out.println("利息为: " + currencyInstance.format(interest));
    }
最后的小总结

上面我们先是讲了Bigdecimal的使用背景,然后讲解了该类常用的方法,包括四则运算和数值间的比较,最后讲了对Bigdecimal类型的格式化方法。
在使用过程中,还需要额外注意以下三点:

  1. Bigdecimal的性能相比doublefloat而言较差,所以如果对于精度要求不高的场景,可以不需要用该类来进行计算。
  2. Bigdecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。
  3. 尽量避免直接用入参为double的构造函数创建Bigdecimal对象

参考文章:
Java之BigDecimal详解: https://www.cnblogs.com/zhangyinhua/p/11545305.html
Java BigDecimal详解: https://www.cnblogs.com/YangJavaer/p/6056394.html

posted @ 2021-01-12 18:48  moutory  阅读(31)  评论(0编辑  收藏  举报  来源