深入 BigDecimal
# 深入 BigDecimal
BigDecimal 是如何存储浮点数,如何实现高精度加法的呢?使用的时候还有什么注意点呢?
# BigDecimal 如何存储浮点数
BigDecimal 类中有 3 个关键的成员变量 intVal
、scale
和 precision
:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
private transient int precision;
//........省略其他代码
}
2
3
4
5
6
当我们使用 BigDecimal
表示 1234.56
时,BigDecimal
中的三个字段会分别以下的内容:
intVal
中存储的是去掉小数点后的全部数字,即123456
;scale
中存储的是小数的位数,即2
;这个值也称为标度。prevision
中存储的是全部的有效位数,小数点前 4 位,小数点后 2 位,即6
;
简单来说,BigDecimal 能做到存储和计算的精确性,就是通过将浮点数转为整数来计算(乘以 10 的 N 次方),来保证存储的精确性;计算时同理,用整数计算后,最后再除以 10 的 N 次方,来保证计算的精确性。
# 创建 BigDecimal 对象的正确方式
BigDecimal 的构造方法有很多,但是使用不当就容易造成错误。我们直接上一段代码:
import java.math.BigDecimal;
public class BigDecimalDemo4 {
public static void main(String[] args) {
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
按理说,用浮点数和字符串创建的对象,其值应该都是 8.8,但我们来看看输出:
> javac BigDecimalDemo4.java -encoding utf8
> java BigDecimalDemo4
88
8.8
8.800000000000000710542735760100185871124267578125
2
3
4
5
为什么使用 double 创建的 BigDecimal 对象,会导致精度不准确? 这得从创建对象的源码说起。我们这里列一些关键的代码。
我们用 double 来创建对象的时候,构造方法为:
/**
在注释里,作者就提到了这个构造方法可能不太精确。并说明了如何规避。我这里仅列出了关键的注释。
The results of this constructor can be somewhat unpredictable.
The Strin constructor, on the other hand, is perfectly predictable:
*/
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
2
3
4
5
6
7
8
9
可以看到其是调用另一个构造方法,我们来看看:
public BigDecimal(double val, MathContext mc) {
if (Double.isInfinite(val) || Double.isNaN(val))
throw new NumberFormatException("Infinite or NaN");
// Translate the double into sign, exponent and significand, according
// to the formulae in JLS, Section 20.10.22.
long valBits = Double.doubleToLongBits(val);
//............省略其他代码
}
2
3
4
5
6
7
8
可以看到,其首先判断 double 的有效性,有效的话就使用 doubleToLongBits
方法 **,**doubleToLongBits
的源码如下:
public static long doubleToLongBits(double value) {
long result = doubleToRawLongBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if ( ((result & DoubleConsts.EXP_BIT_MASK) ==
DoubleConsts.EXP_BIT_MASK) &&
(result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
result = 0x7ff8000000000000L;
return result;
}
2
3
4
5
6
7
8
9
10
11
问题就出在这里:doubleToRawLongBits 就是将 double 转换为 long,这个方法是原始方法(注意其 native 修饰符,底层不是 java 实现,是 c++ 实现的),而 double 是不精确的,BigDecimal 在处理的时候把十进制小数扩大 N 倍让它在整数上进行计算,得到的结果也是不精确的。
更多请参考:Java 中的浮点数
所以,在涉及到精度计算的过程中,我们尽量使用 String 类型来进行转换。
当然,我们也可以使用 BigDecimal.valueOf()
来创建 BigDecimal 对象 。valueOf 方法如果传的是浮点数会调用 valueOf(double val)
这个实现,内部用的就是 new BigDecimal(Double.toString(val))
:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
2
3
但是如果传了个 float 给这个方法,有可能在 float 转换到 double 时发生精度丢失。
在《阿里巴巴 Java 开发手册》中也有这样一条建议,或者说是要求:
11.【强制】禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。
说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。Java 开发手册
如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149
正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了
Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
# 比较 BigDecimal 的正确方式
BigDecimal 的等值比较应使用 compareTo 方法,而不是 equals 方法。
# 使用 equals 方法有什么问题?
我们看个粒子:
BigDecimal bigDecimal = new BigDecimal(1);
BigDecimal bigDecimal1 = new BigDecimal(1);
System.out.println(bigDecimal.equals(bigDecimal1));
BigDecimal bigDecimal2 = new BigDecimal(1);
BigDecimal bigDecimal3 = new BigDecimal(1.0);
System.out.println(bigDecimal2.equals(bigDecimal3));
BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0");
System.out.println(bigDecimal4.equals(bigDecimal5));
2
3
4
5
6
7
8
9
10
11
12
13
以上代码,输出结果为:
true
true
false
2
3
为什么有时候是 true,有时候是 false?这得从 equals 的源码说起。
# 标度的概念
equals 方法的源码如下:
/**
Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two 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)
*/
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
其实在 equal 的注释里,就已经说明了原因。大意是说,只有 intVal 和 scale 都 equal 的时候,equals 方法才认为比较的对象相等。我们之前说过,intVal 就是数值,这很好理解,例如 BigDecimal bigDecimal = new BigDecimal(1);
那么 1 就是数值
scale 我们说过,是小数的位数。scale 翻译成中文有刻度、数值范围的意思,也可以叫标度。一个 BigDecimal 是通过一个"无标度值"和一个"标度"来表示一个数的。例如上面比较标度的代码如下 :
if (scale != xDec.scale)
return false;
2
而我们的示例代码,为什么比较的结果是 false,就是因为标度不一样。
BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0");
System.out.println(bigDecimal4.equals(bigDecimal5)); //false
2
3
bigDecimal4 的标度是 0,bigDecimal5 的标度是 1,感兴趣的读者可以自行用 IDE 的 debug 功能查看调试
截图来自 为什么阿里巴巴禁止使用 BigDecimal 的 equals 方法做等值比较? (opens new window)
# 小结
我们用了大量的篇幅来讲解 BigDecimal,我们现在来简单用几句话总结下吧!
希望读者看到下面的话时能想到对应的知识点,并理解其原理,最后自己动手试试之前提到的 Java 代码
- Java 提供的 double 和 float 类型是不准确的,对于需要精确计算的地方不要使用;
- 商业计算使用 BigDecimal
- BigDecimal 都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
- 尽量使用参数类型为 String 的构造函数。
- 使用 compareTo 方法来比较数值,而不是 equals
# 参考资料
为什么 0.1 + 0.2 = 0.3 - 面向信仰编程 (opens new window)
事故总结集锦-BigDecimal 在金额计算中丢失精度导致的资损事故 10(一周一更) - 掘金 (opens new window)
求求你,不要再让浮点数背锅了 - 简书 (opens new window)
深入理解 BigDecimal - 知乎 (opens new window)
Java BigDecimal 详解_jackiehff 的博客-CSDN 博客_java bigdecimal (opens new window)