BigDecimal的正确使用姿势

219 阅读4分钟

一、问题背景

  在Java中,用来表示浮点数的数据类型有 floatdouble。在涉及到金钱相关的开发需求当中,它的计算结果会怎样。先看几个简单的浮点数四则运算:

public static void main(String[] args) {

    System.out.println(0.1 + 0.2);              // 0.30000000000000004
    System.out.println(1.0 - 0.8);              // 0.19999999999999996
    System.out.println(4.015 * 100);            // 401.49999999999994
    System.out.println(123.3 / 100);            // 1.2329999999999999
    System.out.println((2.15 - 1.10) == 1.05);  // false
}

  从程序计算结果来看,和我们的预期是有很大差距的。比如 0.1 + 0.2 的结果并不是 0.3,造成这种现象的主要原因是:计算机是以二进制来存储数值的,浮点数也不例外0.1 的二进制表示为: 0.0 0011 0011 0011 ....,转换为十进制为: 0.1000000000000000055511151231257827021181583404541015625。可以看到的 0.1的二进制表示是个无限循环的,那么这个无限循环对计算机来讲是没法精确表示,这就造成了浮点数的精度损失的原因。为了尽可能的降低精度损失带来的计算误差,Java 中为我们提供了BigDecimal提高精度

public static void main(String[] args) {
    
    // 0.3000000000000000166533453693773481063544750213623046875
    System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));

    // 0.1999999999999999555910790149937383830547332763671875
    System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));

    // 401.49999999999996802557689079549163579940795898437500
    System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));

    // 1.232999999999999971578290569595992565155029296875
    System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));
}

  可以看到,0.1 + 0.2new BigDecimal(0.1).add(new BigDecimal(0.2))的计算结果还是存在误差,只是 BigDecimal 的结果更加精确,那么 BigDecimal 为什么没有达到我们想要的预期呢?这就要了解BigDecimal的一些使用技巧。

二、BigDecimal使用技巧

2.1 必须使用字符串的构造方法来初始化BigDecimal对象

public static void main(String[] args) {
    
    // 0.3
    System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
    
    // 0.2
    System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
    
    // 401.500
    System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
    
    // 1.233
    System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));
}

  为什么在使用了 public BigDecimal(String val)之后,计算结果就符合我们的预期了呢?BigDecimal的两个重要属性

public class BigDecimal extends Number implements Comparable<BigDecimal> {

    // 表示小数点后的的位数,也就是保留几位小数
    private final int scale;                         
    /**
     * 表示精度,也就是有效数字的长度,什么是有效数字长度?举个例子:
     * 12345 / 100000 = 0.12345 则 scale = 5  precision = 5
     * 1 / 100000 = 0.00001 则 scale = 5  precision = 1
     */
    private transient int precision;
}    

所以在 public BigDecimal(String val) 字符的构造函数中,根据字符串的长度解析得到 scaleprecision

2.2 统一使用compareTo方法来比较BigDecimal的大小

public static void main(String[] args) {

    BigDecimal a = new BigDecimal("0.01");
    BigDecimal b = new BigDecimal("0.010");

    System.out.println(a.equals(b));           // false
    System.out.println(a.compareTo(b) == 0);   // true
}

因为BigDecimalequals方法会同时比较 valuescale

2.3 BigDecimal的加减乘除运算时最好都设置保留小数位数和舍入模式

  • 舍入模式参考 java.math.RoundingMode 类,比如

image.png

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("3.0");

    System.out.println(a.divide(b));
}

运行时报 ```java.lang.ArithmeticException````

Exception in thread "main" java.lang.ArithmeticException:
        Non-terminating decimal expansion; no exact representable decimal result.
	at java.math.BigDecimal.divide(BigDecimal.java:1690)

所以为了避免这种错误,建议按照如下方式使用:

  • 针对 divide方法,运算过程中一定要设置好小数位数和舍入模
public static void main(String[] args) {

    BigDecimal divide = new BigDecimal("1.0").divide(new BigDecimal("3.0"), 2, RoundingMode.UP);
    System.out.println(divide);  //0.34
}    
  • 针对 addsubtractmultiply 加减乘的方法,对运算结果一定要调用setScale方法设置好小数位数和舍入模式
public static void main(String[] args) {

    // 0.3000000000000000166533453693773481063544750213623046875
    BigDecimal add = new BigDecimal(0.1).add(new BigDecimal(0.2)).setScale(2, RoundingMode.UP);
    System.out.println(add);    // 0.31

    // 0.1999999999999999555910790149937383830547332763671875
    BigDecimal sub = new BigDecimal(1.0).subtract(new BigDecimal(0.8)).setScale(2, RoundingMode.UP);
    System.out.println(sub);    // 0.20

    // 401.49999999999996802557689079549163579940795898437500
    BigDecimal mul = new BigDecimal(4.015).multiply(new BigDecimal(100)).setScale(2, RoundingMode.UP);
    System.out.println(mul);    // 401.50
}

2.4 统一使用toPlainString()方法来打印 BigDecimal结果

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("4536785E10");

    // 4.536785E+16
    System.out.println(a.toString());
    
    // 45.36785E+15
    System.out.println(a.toEngineeringString());

    // 45367850000000000
    System.out.println(a.toPlainString());
}
  • toString()toEngineeringString()会使用科学计数法来输出结果
  • toPlainString() 会真正的输出实际结果值

2.5 BigDecimal计算结果格式化

数值格式化主要有:NumberFormat 和 DecimalFormat,这里我就不再叙述,详情可参考博文: www.cnblogs.com/miniSimple/…

三、数值计算防止溢出

数值计算还有一个要小心的点是溢出,不管是 int 还是 long,都有超出表达范围的可能性。如下代码案例:

public static void main(String[] args) {
    
    long l = Long.MAX_VALUE;
    // -9223372036854775808
    System.out.println(l + 1); 
}

显然这个计算结果是不对的,但是没有任何异常信息,在实际开发中这个是很危险的!

  • 使用 Math 类的 addExactsubtractExactxxExact 方法进行数值运算,这些方法可以在数值溢出时主动抛出异常
public static void main(String[] args) {

    long l = Long.MAX_VALUE;
    System.out.println(Math.addExact(l, 1));
}

image.png

  • 使用BigInteger BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。
public static void main(String[] args) {

    BigInteger big = new BigInteger(String.valueOf(Long.MAX_VALUE));

    // 9223372036854775808
    System.out.println(big.add(BigInteger.valueOf(1)).toString());

    System.out.println(big.add(BigInteger.valueOf(1)).longValueExact());
}

image.png

四、总结

总之,对于金融、支付等场景,业务需要时请尽可能使用 BigDecimal 或者 BigInteger,避免由精度和溢出引起重大的 Bug。