深入BigDecimal
# 38.深入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有很多的构造方法,例如:
- BigDecimal(int) 创建一个具有参数所指定整数值的对象
- BigDecimal(double) 创建一个具有参数所指定双精度值的对象
- BigDecimal(long) 创建一个具有参数所指定长整数值的对象
- BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象
以上四个方法,创建出来的的BigDecimal的标度是不同的。
其中,BigDecimal(long) 和BigDecimal(int),因为是整数,所以标度就是0 :(参考源码1084行和1129行)
public BigDecimal(int val) {
this.intCompact = val;
this.scale = 0;
this.intVal = null;
}
public BigDecimal(long val) {
this.intCompact = val;
this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
this.scale = 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
而对于BigDecimal(double) ,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的,而是0.1000000000000000055511151231257827021181583404541015625 。这是因为doule自身表示的只是一个近似值。
那么他的标度就是这个数字的位数,即55。
截图来自 为什么阿里巴巴禁止使用BigDecimal的equals方法做等值比较? (opens new window)
其他的浮点数也同样的道理。对于new BigDecimal(1.0)这样的形式来说,因为他本质上也是个整数,所以他创建出来的数字的标度就是0。
所以,因为BigDecimal(1.0)和BigDecimal(1.00)的标度是一样的,所以在使用equals方法比较的时候,得到的结果就是true。
而对于BigDecimal(double) ,当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。那么他的标度也就是1。
如果使用new BigDecimal("0.10000"),那么创建出来的数就是0.10000,标度也就是5。
所以,因为BigDecimal("1.0")和BigDecimal("1.00")的标度不一样,所以在使用equals方法比较的时候,得到的结果就是false。
# compareTo
如果我们只想判断两个BigDecimal的值是否相等,那么该如何判断呢?BigDecimal中提供了compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回0。
BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0000");
System.out.println(bigDecimal4.compareTo(bigDecimal5)); //输出true
2
3
附上compareTo的源码
public int compareTo(BigDecimal val) {
// Quick path for equal scale and non-inflated case.
if (scale == val.scale) {
long xs = intCompact;
long ys = val.intCompact;
if (xs != INFLATED && ys != INFLATED)
return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
}
int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign)
return (xsign > ysign) ? 1 : -1;
if (xsign == 0)
return 0;
int cmp = compareMagnitude(val);
return (xsign > 0) ? cmp : -cmp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17