BigDecimal踩过的坑

丢失精度问题

问题分析

首先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void t1() {
BigDecimal bigDecimal = new BigDecimal(88);
System.out.println(bigDecimal);
bigDecimal = new BigDecimal("8.8");
System.out.println(bigDecimal);
bigDecimal = new BigDecimal(8.8);
System.out.println(bigDecimal);
bigDecimal = BigDecimal.valueOf(8.8);
System.out.println(bigDecimal);
}

上述代码输出结果为:
88
8.8
8.800000000000000710542735760100185871124267578125
8.8

通过测试发现,当使用 double 或者 float 这些浮点数据类型时,会丢失精度,String、int 则不会

1
2
3
4
5
6
double 之所以会出问题,是因为小数点转二进制丢失精度
BigDecimal 在处理的时候把十进制小数扩大N倍让它在整数上进行计算,并保留相应的精度信息。
①float 和 double 类型,主要是为了科学计算和工程计算而设计的,之所以执行二进制浮点运算,是为了在广泛的数值范围上提供较为精确的快速近和计算。
②并没有提供完全精确的结果,所以不应该被用于精确的结果的场合。
③当浮点数达到一定大的数,就会自动使用科学计数法,这样的表示只是近似真实数而不等于真实数。
④当十进制小数位转换二进制的时候也会出现无限循环或者超过浮点数尾数的长度。

总结

所以大家要尽量要使用字符串而不是浮点数去构造 BigDecimal 对象,如果实在不行,就使用 BigDecimal#valueOf() 方法吧。

1
2
3
4
5
6
7
@Test
public void t2() {
BigDecimal bigDecimal2 = new BigDecimal("8.8");
BigDecimal bigDecimal3 = new BigDecimal("8.812");
System.out.println(bigDecimal2.compareTo(bigDecimal3));
System.out.println(bigDecimal2.add(bigDecimal3));
}

BigDecimal转回String 要小心

问题展示

1
2
3
4
5
6
7
@Test
public void t3() {
BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
String out = d.toString();
// 1.23345353454567E+16
System.out.println(out);
}

总结

可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果 BigDecimal 有三个方法可以转为相应的字符串类型,切记不要用错:

1
2
3
4
// 不使用科学计数法
System.out.println(d.toPlainString());
// 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数
System.out.println(d.toEngineeringString());

执行顺序不能调换(乘法交换律失效)

问题分析

乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况:

1
2
3
4
5
6
7
8
@Test
public void t4() {
BigDecimal a = BigDecimal.valueOf(1.0);
BigDecimal b = BigDecimal.valueOf(3.0);
BigDecimal c = BigDecimal.valueOf(3.0);
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP).multiply(c)); // 0.990
System.out.println(a.multiply(c).divide(b, 2, RoundingMode.HALF_UP)); // 1.00
}

总结

容易产生无限精度的运算放在最后,再四舍五入保留小数

BigDecimal最佳实践

关于金额计算,很多业务团队会基于 BigDecimal 再封装一个 Money 类,其实我们直接可以用一个半官方的 Money 类:JSR 354,虽然没能在 Java 9 中成为 Java 标准,很有可能集成到后续的 Java 版本中成为官方库。

  • maven 坐标
1
2
3
4
5
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>1.1</version>
</dependency>
  • 新建 Money 类
1
2
3
4
CurrencyUnit cny = Monetary.getCurrency("CNY");
Money money = Money.of(1.0, cny);
// 或者 Money money = Money.of(1.0, "CNY");
//System.out.println(money);
  • 金额运算
1
2
3
4
5
CurrencyUnit cny = Monetary.getCurrency("CNY");
Money oneYuan = Money.of(1.0, cny);
Money threeYuan = oneYuan.add(Money.of(2.0, "CNY")); //CNY 3
Money tenYuan = oneYuan.multiply(10); // CNY 10
Money fiveFen = oneYuan.divide(2); //CNY 0.5
  • 比较相等
1
2
3
Money fiveFen = Money.of(0.5, "CNY"); //CNY 0.5
Money anotherFiveFen = Money.of(0.50, "CNY"); // CNY 0.50
System.out.println(fiveFen.equals(anotherFiveFen)); // true

可以看到,这个类对金额做了显性的抽象,增加了金额的单位,也避免了直接使用 BigDecimal 的一些坑。