开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
介绍
大多数企业应用程序都使用浮点值。
金融科技、电商、金融等应用每天都在处理浮点运算,需要对所有计算进行完整的精度控制。
开发人员需要知道哪种 Java 数据类型适合表示浮点值,尤其是货币值。
让我们试着弄明白。
为什么 double 不准确
首先想到的是使用 float 和 double 数据类型。
但是,如果需要精确值,则不建议使用这些数据类型。事实上,float 和 double 类型主要用于科学和工程计算。
他们实施精心设计的二进制浮点运算,以快速获得广泛值的正确近似值。
不幸的是,这些类型没有给出准确的结果,因此不适合货币计算:
double val = 1.03 - .42;
System.out.println(val); // 0.6100000000000001
要更深入地了解浮点数的表示方式,请查看IEEE 浮点运算标准。
BigDecimal 是如何保证准确性的
回到介绍,让我们看看 BigDecimal 是如何表示一个数字的,它是如何保证精度的。
查看BigDecimal的源码,我们可以发现一个BigDecimal是通过一个未缩放的值和一个小数位数来表示的:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; // unscaled value
private final int scale; // scale
private final transient long intCompact;
}
scale 字段表示 BigDecimal 的小数位数。
未缩放的值使用稍微复杂的表示。
当未缩放的值超过阈值(默认为 Long.MAX_VALUE)时,使用 intVal 字段存储该值,intCompact 字段存储 Long.MIN_VALUE,用于指示尾数信息只能从 intVal 获得。
否则,将未缩放的值紧凑地存储在long类型的intCompact字段中,用于后续计算,并且intVal为空。
BigDecimal 还提供了一个 scale 方法来返回 BigDecimal 的小数位数:
public int scale() {
return scale;
}
该方法的注释详细描述了比例值的使用方式:
返回此 BigDecimal 的小数位数。如果为零或正数,则小数位数是小数点右边的位数。如果为负数,则将数字的未缩放值乘以 10 的缩放负数次方。例如,-3 的标度表示未标度的值乘以 1000。
回到上面的例子,无法用二进制精确表示的数字 0.61 可以使用 BigDecimal 表示,未缩放的值为 61,缩放为 2。
如何正确创建 BigDecimal
查看BigDecimal的源码,我们可以找出四个重要的构造函数:
public BigDecimal(int val)
public BigDecimal(long val)
public BigDecimal(double val)
public BigDecimal(String val)
上面四个构造函数创建的BigDecimal的scale是不一样的。BigDecimal(int)并且BigDecimal(long)更直接并采用整数输入参数,因此它们的比例均为 0。
BigDecimal(double)并且BigDecimal(String)比例需要更详细的考虑。
BigDecimal(double) 有什么问题
BigDecimal 提供了一种从 double 创建 BigDecimal 的方法,但不推荐这样做,因为结果可能有些不可预测。使用双精度值 0.61 创建的 BigDecimal 并不完全等于 0.61,因为 0.61 不能精确表示为有限长度的二进制。实际表示是:
BigDecimal val = new BigDecimal(0.61); // 0.60999999999999998667732370449812151491641998291015625
因此,使用 BigDecimal(double),特别是涉及金融交易的计算,是极其危险的,因为它可能会失去精度。
使用 BigDecimal(String) 创建
构造BigDecimal(String)函数是完全可以预测的。该行new BigDecimal("0.61")创建了一个 BigDecimal,它正好等于 0.61:
BigDecimal val = new BigDecimal("0.61"); // 0.61
但是必须注意的是new BigDecimal("0.610000"),new和new的尺度BigDecimal("0.61")分别是6和2。这些BigDecimals的equals方法比较的结果是false。
相应地,有以下两种方法可以创建一个可以准确表示0.61的BigDecimal:
BigDecimal recommend1 = new BigDecimal("0.61");
BigDecimal recommend2 = BigDecimal.valueOf(0.61);
该方法BigDecimal.valueOf(double)采用两步流程,通过调用Double.toString(double)方法实现。第一步是将 Double 转换为 String。第二步是将 String 转换为 BigDecimal:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
结论
由于计算机使用二进制代码来存储和处理数据,因此许多浮点数不能精确地表示为任何有限长度的二进制小数。
因此,在计算机中采用了一种通过近似表示浮点数的方式,如单精度浮点数和双精度浮点数格式。
不幸的是,这些类型不能给出准确的结果,也不适合精确计算,尤其是影响金融交易。
因此,使用BigDecimal(double)是极其危险的,因为它可能会失去精度和不可预测的结果。
因此,通常首选和强烈推荐的创建 BigDecimal 的方法是使用BigDecimal(String)和BigDecimal.valueOf(double)方法。