持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
序言
小数计算为什么要用BigDecimal而不是Double直接算
这是一道经常会被问到八股文,大家都知道Double会丢精度,那么为什么会丢精度,了解这个问题我们需要先知道十进制转二进制这些知识点。
十进制小数转二进制
整数转二进制的方法是,除2取余,小数乘2取整,我们在一个表格中表示一下,假设0.625这个值。
| 步骤 | 0.625 | 十进制转二进制 | |
|---|---|---|---|
| 1 | 0.625 * 2 = 1.25 | 取1 | 余0.25 |
| 2 | 0.25 * 2 = 0.5 | 取0 | 余0.5 |
| 3 | 0.5 * 2 = 1.0 | 取1 | 余0 |
到这里从上往下数,也就是0.101就是0.625的二进制表示法。这么一看好像也没有什么问题。那问题出在哪里呢,我们来假设一个数:0.3,用上面的方法,再来计算一遍。
| 步骤 | 0.3 | 十进制转二进制 | |
|---|---|---|---|
| 1 | 0.3 * 2 = 0.6 | 取0 | 余0.6 |
| 2 | 0.6 * 2 = 1.2 | 取1 | 余0.2 |
| 3 | 0.2 * 2 = 0.4 | 取0 | 余0.4 |
| 4 | 0.4 * 2 = 0.8 | 取0 | 余0.8 |
| 5 | 0.8 * 2 = 1.6 | 取1 | 余0.6 |
这时发现问题了,最后余数0.5,无限循环了,那这个时候我们的系统就会取前几位了,也就是我们在数学中的约等于符号(≈),这也就是我们导致丢失精度的主要原因。
BigDecimal 是怎么保存的,为什么他的计算不会丢失精度。
要探究其原理,先要阅读一下BigDecimal在创建时候的构造方法源码。
本文中的中文注释都是源码中的注释翻译过来的
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
我们通常调用的是BigDecimal的这个构造函数,发现里面调用了另外一个构造,接着往里追。
public BigDecimal(char[] in, int offset, int len) {
this(in,offset,len,MathContext.UNLIMITED);
}
public static final MathContext UNLIMITED =
new MathContext(0, RoundingMode.HALF_UP);
这个构造方法中,又去调用了另外的构造方法,这里不一样的是多加了一个MathContext,接着往下追。
public BigDecimal(char[] in, int offset, int len, MathContext mc)
这个方法中的代码有些长,下面采取分段的方式进行讲解。
构造(char[] in, int offset, int len, MathContext mc)
在此构造方法中表示,声明了三个变量,prec, scl, rs,分别对应了BigDecimal中的三个成员变量。
private final int scale;
private transient int precision;
private final transient long intCompact;
关于scale和precision变量的解释,可以借鉴。 点次前往查看
谈谈我个人的理解,prec是这个数字的全部个数。scl是小数点后的数字个数至于intCompact成员变量也就是rs,从上述代码可以看出,是把所有的去除了小数点,变成整数放在这个变量中。
以 3.44 这个数字举例。
rc: 344, prec: 3, scl: 2
从这里就可以得知,BigDecimal保存数字的方式发生了改变,所以自然在运算的时候就不会发生小数点循环的问题了。