小数计算为什么要用BigDecimal而不是使用Double直接运算

577 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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)

image.png 在此构造方法中表示,声明了三个变量,prec, scl, rs,分别对应了BigDecimal中的三个成员变量。

private final int scale;  

private transient int precision;

private final transient long intCompact;

关于scale和precision变量的解释,可以借鉴。 点次前往查看
谈谈我个人的理解,prec是这个数字的全部个数。scl是小数点后的数字个数 image.png 至于intCompact成员变量也就是rs,从上述代码可以看出,是把所有的去除了小数点,变成整数放在这个变量中。

以 3.44 这个数字举例。
rc: 344, prec: 3, scl: 2

从这里就可以得知,BigDecimal保存数字的方式发生了改变,所以自然在运算的时候就不会发生小数点循环的问题了。