为什么在阿里云面试中建议使用BigDecimal进行浮点数运算

在最近的技术面试中,BigDecimal 成为了大厂面试中的常考知识点之一。根据《阿里巴巴 Java 开发手册》的指导:“为了避免精度丢失,建议使用 BigDecimal 来进行浮点数的运算”。

浮点数运算真的有可能导致精度丢失吗?答案是肯定的!

精度丢失的示例

以下代码展示了浮点数计算可能导致的精度问题:

float a = 2.0f - 1.9f;  
float b = 1.8f - 1.7f;  
System.out.println(a); // 0.100000024  
System.out.println(b); // 0.099999905  
System.out.println(a == b); // false  

那么,为什么在使用 floatdouble 进行运算时会导致精度丢失呢?

这与计算机存储浮点数的方式密切相关。计算机内部使用二进制表示数字,并且在存储时其宽度是有限的。当遇到无限循环的小数时,计算机会截断这些小数,从而引发精度损失。这就是为什么浮点数无法在二进制中精确表示的原因。例如,十进制中的 0.2 无法准确转换为二进制小数。

浮点数的二进制转换过程示例

// 0.2 转换为二进制数的过程如下:
0.2 * 2 = 0.4 -> 0  
0.4 * 2 = 0.8 -> 0  
0.8 * 2 = 1.6 -> 1  
0.6 * 2 = 1.2 -> 1  
0.2 * 2 = 0.4 -> 0(循环开始)  
...

欲了解更多关于浮点数的知识,建议阅读《计算机系统基础(四)浮点数》这篇文章。

BigDecimal 的优势

BigDecimal 的最大优点在于它可以进行浮点数运算而不会造成精度丢失。在大部分需要精确浮点数结果的业务场景下,例如财务计算,通常会选择 BigDecimal

根据《阿里巴巴 Java 开发手册》的建议,进行浮点数等值判断时,基本数据类型不应使用 == 进行比较,而包装数据类型则不应使用 equals 方法。具体原因在前文已经详细阐述,这里不再赘述。

为了解决浮点数运算精度丢失的问题,可以通过 BigDecimal 来定义浮点数并进行运算。

使用 BigDecimal 进行浮点数运算的示例

BigDecimal a = new BigDecimal("1.0");  
BigDecimal b = new BigDecimal("0.9");  
BigDecimal c = new BigDecimal("0.8");  
  
BigDecimal x = a.subtract(b);  
BigDecimal y = b.subtract(c);  
  
System.out.println(x.compareTo(y)); // 0  

BigDecimal 的常用方法介绍

创建 BigDecimal

为了防止精度丢失,我们推荐使用 BigDecimal(String val) 构造方法或 BigDecimal.valueOf(double val) 静态方法来创建 BigDecimal 对象。

基本运算:加减乘除

  • add: 将两个 BigDecimal 对象相加
  • subtract: 将两个 BigDecimal 对象相减
  • multiply: 将两个 BigDecimal 对象相乘
  • divide: 将两个 BigDecimal 对象相除

示例代码如下:

BigDecimal a = new BigDecimal("1.0");  
BigDecimal b = new BigDecimal("0.9");  
System.out.println(a.add(b)); // 1.9  
System.out.println(a.subtract(b)); // 0.1  
System.out.println(a.multiply(b)); // 0.90  
System.out.println(a.divide(b)); // 会抛出 ArithmeticException 异常  
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP)); // 1.11  

注意:使用 divide 方法时,尽量选择带有三个参数的版本,并设置合适的 RoundingMode,以避免 ArithmeticException(当遇到无法除尽的无限循环小数时)。

大小比较

使用 compareTo 方法对两个 BigDecimal 进行比较:

BigDecimal a = new BigDecimal("1.0");  
BigDecimal b = new BigDecimal("0.9");  
System.out.println(a.compareTo(b)); // 1  

设置保留小数位

通过 setScale 方法设置小数位数和保留规则。

BigDecimal m = new BigDecimal("1.255433");  
BigDecimal n = m.setScale(3, RoundingMode.HALF_DOWN);  
System.out.println(n); // 1.255  

BigDecimal 的等值比较问题

在使用 equals() 方法进行比较时,可能会出现意想不到的结果:

BigDecimal a = new BigDecimal("1");  
BigDecimal b = new BigDecimal("1.0");  
System.out.println(a.equals(b)); // false  

这是因为 equals() 方法不仅比较数值,还比较精度(scale),而 compareTo() 方法只比较值。

例如,1.0 的 scale 是 1,而 1 的 scale 是 0,因此 a.equals(b) 返回 false。

而使用 compareTo() 方法比较时,若两个数相等则返回 0。

BigDecimal a = new BigDecimal("1");  
BigDecimal b = new BigDecimal("1.0");  
System.out.println(a.compareTo(b)); // 0  

总结

由于浮点数无法在二进制中精确表示,因此存在精度丢失的风险。然而,Java 提供了 BigDecimal 来安全地处理浮点数。BigDecimal 的实现依赖于 BigInteger,同时引入了小数位的概念,使得它在浮点数运算中成为更为合适的选择。

参考资料

[1] 计算机系统基础(四)浮点数: [http://kaito-kidd.com/2018/08/08/computer-system-float-point/](