BigDecimal 类的四个常见陷阱,请别再这么使用了!
陷阱 1:使用 double 构造函数
在日常使用 Java 中进行业务计算,尤其是涉及货币,金额时,建议使用 BigDecimal
类,以避免使用 float
或 double
等原始类型所遇到的浮点数运算精度问题。
请看下面这个例子:
BigDecimal x = new BigDecimal(0.1);
System.out.println("x=" + x); //输出结果 x=0.1000000000000000055511151231257827021181583404541015625
结果发现 此构造函数的结果可能不准确。这是因为浮点数在计算机硬件中表示为以2为底的(二进制)分数。然而,大多数十进制分数不能精确地表示为二进制分数。实际存储在计算机中的二进制浮点数仅近似于输入的十进制浮点数。因此,传递给double构造函数的值并不完全等于0.1。
相比之下,String构造函数是完全可预测的,它产生的BigDecimal正好等于0.1,正如预期的那样。
BigDecimal y = new BigDecimal("0.1");
System.out.println("y=" + y); //输出结果 y=0.1
所以, 应该优先使用String构造函数而不是double构造函数。
如果必须使用double来创建BigDecimal,请使用静态BigDecimal.valueOf(double)
方法。
底层会使用Double.toString(double)
方法然后使用BigDecimal(String)
构造函数将double转换为String相同的结果。
陷阱 2:使用静态方法 valueOf(double)
如果使用静态BigDecimal. valueof (double)
方法创建BigDecimal,请注意这个方法返回值精度有限
BigDecimal x = BigDecimal.valueOf(1.01234567890123456789);
BigDecimal y = new BigDecimal("1.01234567890123456789");
System.out.println("x=" + x);
System.out.println("y=" + y);
//输出结果
// x=1.0123456789012346
// y=1.01234567890123456789
x值丢失了四位小数,因为double的精度只有15-17位(float的精度只有6-9位),而BigDecimal的精度是任意的(仅受内存限制)。
因此,使用String构造函数实际最佳实践,因为有效地避免了双构造函数引起的两个主要问题。
陷阱 3:使用 equals(BigDecimal) 方法
即使两个 BigDecimal
值在数值上相等,但由于它们的小数位数(scale)不同,使用 equals()
比较时仍可能返回 false。
BigDecimal x = new BigDecimal("1");
BigDecimal y = new BigDecimal("1.0");
System.out.println(x.equals(y)); // false
System.out.println(x.compareTo(y) == 0); // true
x.equals(y)
为什么等于false
呢?
这是由于BigDecimal由一个任意精度的未缩放整数值和一个32位整数组成,这两个值都必须等于正在比较的另一个BigDecimal的相应值。在这种情况下
-
x的值为1,精度值为0。
-
y的值10, 精度的1。
因此,x不等于y。
因此,不应使用equals()
方法比较BigDecimal的两个实例,而应使用compareTo()
方法,因为它比较BigDecimal两个实例表示的数值(x=1;y=1.0)。这里有一个例子:
System.out.println(x.compareTo(y) == 0); // 输出 true
所以大家在开发过程中应使用 compareTo()
进行数值相等比较;如果一定要用equals()
方法需要同时考虑值和小数位数。
陷阱 4:使用 round(MathContext) 方法
大家在开发过程中 可能会试图使用round(new MathContext(precision,routingMode))
方法将BigDecimal四舍五入到(假设)两位小数,这种方法会出问题的。
round()
方法是基于有效数字进行舍入,而不是基于小数位数。
BigDecimal x = new BigDecimal("12345.6789");
x = x.round(new MathContext(2, RoundingMode.HALF_UP));
System.out.println(x.toPlainString()); // 输出: 12000
System.out.println(x.scale()); // 输出: -3
该方法不会对小数部分进行舍入,但会将未缩放的值四舍五入到给定的有效位数(从左到右计数),保持小数点不变,在上面的示例中,这会导致-3的负小数位数。
这是为什么?
未缩放的值(123456789)四舍五入为两位有效数字(12),表示精度为2。但是,由于小数点保持不变,因此此BigDecimal表示的实际值为120000000。这也可以写成12000,因为小数点右侧的四个零没有意义。
但是精度呢?为什么它是-3
,而不是0?
这是因为这个BigDecimal的未缩放值是12,因此,它必须乘以1000,即10的3次方,(12 x 103)等于12000。
因此,正刻度表示小数位数(即小数点右侧的位数),而负刻度表示小数点左侧不重要的位数(在这种情况下,尾随的零,因为它们只是表示数字刻度的占位符)。
因此,最后,BigDecimal表示的数字是未缩放的Value x 10小数位数。
还要注意,上面的代码使用了toPlainString()
方法,该方法不以科学符号(1.2E+4)显示结果。
要获得12345.68的预期结果,请尝试setScale(scale,roundingMode)
方法,例如
BigDecimal x = new BigDecimal("12345.6789");
x = x.setScale(2, RoundingMode.HALF_UP);
System.out.println("x=" + x)); //输出 x=12345.68
setScale(scale,roundingMode)
方法根据指定的舍入模式将分数部分舍入到小数点后两位。
还可以使用round(new MathContext(precision, roundingMode))
方法进行常规舍入。
但这需要你知道计算结果小数点左侧的数字总数。以下示例:
BigDecimal a = new BigDecimal("12345.12345");
BigDecimal b = new BigDecimal("23456.23456");
BigDecimal c = a.multiply(b);
System.out.println("c=" + c); //输出 c=289570111.3153564320
例如,要将c
四舍五入到小数点后两位,必须使用精度为11的MathContext对象
BigDecimal d = c.round(new MathContext(11, RoundingMode.HALF_UP));
System.out.println("d=" + d); //d=289570111.32
小数点左侧的总位数可以这样计算
bigDecimal.precision() - bigDecimal.scale() + newScale
其中bigDecimal.precision()
是未舍入结果的精度。scale()
是未舍入结果的比例。
newScale
是要舍入到的比例。那么最后完整代码
BigDecimal e = c.round(new MathContext(c.precision() - c.scale() + 2, RoundingMode.HALF_UP));
//结果输出 e=289570111.32
然而,如果你比较下面这个表达式代码
c.setScale(2, RoundingMode.HALF_UP);
为了确保代码的可读性和简洁性,选择这种代码就更好。
所以如需保留两位小数,应使用 setScale(2, RoundingMode.HALF_UP)
,而非 round()
。
扩展 BigDecimal:自定义类 Decimal
你可以将 BigDecimal
扩展为一个自定义类(如 Decimal
),以封装最佳实践。
- 添加构造函数来规避
double
精度问题 - 重写返回类型为
BigDecimal
的方法,使其返回Decimal
- 添加新方法如
equalTo()
,greaterThan()
等,提升可读性
public Decimal(double val) {
super(Double.toString(val));
if (precision() > 14) {
throw new IllegalArgumentException("Potential precision loss");
}
}
格式化与舍入辅助方法
使用接口和枚举可以标准化舍入与格式化的逻辑。
RoundingInfo
接口定义舍入规则FormatInfo
接口定义格式模板和地区- 可创建
Rounding.AMOUNT
,Format.AMOUNT
等常用预设
Decimal c = a.multiply(b).round(Rounding.AMOUNT);
System.out.println("c=" + c.format(Format.AMOUNT)); // 输出: 289,570,111.32
结论
BigDecimal
是处理精确小数的关键类,但使用不当容易出错。通过:
- 避免使用
double
构造函数 - 使用
compareTo()
而非equals()
- 使用
setScale()
进行舍入 - 自定义类封装常用逻辑