【线上踩坑分享】你能正确使用BigDecimal吗?

64 阅读3分钟

前言

先来看一个常见的问题

打印下面这两个计算结果,得到的值会不会和你预期的并不一样呢?

 System.out.println(0.1 + 0.2);
 System.out.println(1.0 - 0.8);

计算结果:

0.30000000000000004
0.19999999999999996

之所以造成这种问题的主要原因就在于计算机无法精确表达某些数值,因此在涉及到金额计算的时,我们一般会使用BigDecimal来替代,不过它也存在一些需要特别注意的地方,否则一样会遇到一些令人匪夷所思的现象。

问题一:请使用字符串类型的构造方法

如果你使用double类型的构造方法做计算,同样存在精度问题,请看如下的示例代码:

System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));

计算结果:

0.3000000000000000166533453693773481063544750213623046875

换成使用字符串类型的构造方法

 System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));

计算结果:

0.3

问题二:请指定期望的精度以及取舍方式

请看如下示例代码,随着不同的参数值,其精度结果也不同。

System.out.println(new BigDecimal("1.1").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("1.11").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("1.111").multiply(new BigDecimal("100")));
System.out.println("==================");
System.out.println(new BigDecimal("1.1").multiply(new BigDecimal(Double.toString(100))));
System.out.println(new BigDecimal("1.11").multiply(new BigDecimal(Double.toString(100))));
System.out.println(new BigDecimal("1.111").multiply(new BigDecimal(Double.toString(100))));

计算结果:

110.0
111.00
111.100
==================
110.00
111.000
111.1000

实际上这是因为scale和precision的原因,scale表示小数点之后的位数,precision表示精度,计算过程中传入不同的数值会直接影响scale和precision的数值,所以建议在计算时一定要显示的指定精度以及精度的截取方式。

比如,像下面这样,指定保留3位小数,超过3位的部分直接舍弃。

System.out.println(new BigDecimal("1.11").multiply(new BigDecimal("10")).setScale(3, BigDecimal.ROUND_DOWN));
System.out.println(new BigDecimal("1.111").multiply(new BigDecimal("10")).setScale(3, BigDecimal.ROUND_DOWN));
System.out.println(new BigDecimal("1.1111").multiply(new BigDecimal("10")).setScale(3, BigDecimal.ROUND_DOWN));
System.out.println("==================");
System.out.println(new BigDecimal("1.11").multiply(new BigDecimal(Double.toString(10))).setScale(3, BigDecimal.ROUND_DOWN));
System.out.println(new BigDecimal("1.111").multiply(new BigDecimal(Double.toString(10))).setScale(3, BigDecimal.ROUND_DOWN));
System.out.println(new BigDecimal("1.1111").multiply(new BigDecimal(Double.toString(10))).setScale(3, BigDecimal.ROUND_DOWN));

计算结果:

11.100
11.110
11.111
==================
11.100
11.110
11.111

实际上,未指定精度还有可能会直接出现异常的情况。

因为1/3是一个无限小数,所以你像下面这样运行这段代码,则系统会抛出异常。

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("1");
    BigDecimal b = new BigDecimal("3");
    System.out.println(a.divide(b));
}

抛出的异常:Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

可以改成这样:a.divide(b, 2, BigDecimal.ROUND_DOWN),表示保留小数点后2位,多余部分直接舍弃。

问题三:请使用compareTo进行比较

比较两个数值是否相等也是非常常见的问题的,如果你不小心使用了equals来进行比较,可能也会得到意外的结果,比如像下面这样,返回计算结果为false,但实际上在数学上应该为true

System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")));

看看BigDecimal提供的equals方法说明就清楚了,它除了比较value属性之外,还会比较scale属性。

Compares this {@code BigDecimal} with the specified
{@code Object} for equality.  Unlike {@link
#compareTo(BigDecimal) compareTo}, this method considers two
{@code BigDecimal} objects equal only if they are equal in
value and scale (thus 2.0 is not equal to 2.00 when compared by
this method).

因此,如果我们只是希望比较value属性,则建议使用comparTo方法,其返回结果按照标准约定来,-1、0、1分别代表小于、等于、大于

System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1")));

问题四:请注意科学计数法的问题

因为BigDecimal对象默认采用科学计数法来输出,所以对于过大或过小的数字,会采用指数记数法输出,如果要避免出现这样的问题,可以使用toPlainString()方法。

public static void main(String[] args) {
    BigDecimal bd1 = new BigDecimal("12345678901234567890");
    System.out.println(bd1); // 输出 12345678901234567890
    BigDecimal bd2 = new BigDecimal("0.000000001234567890");
    System.out.println(bd2); // 输出 1.23456789E-9
    System.out.println(bd2.toPlainString()); // 输出 0.000000001234567890
}