【优雅的避坑】你的钱算错了!为什么0.1+0.2不等于0.3了!?

853 阅读4分钟

问题初现

我碰到过这样一个问题,对项目上用车记录中的用车里程、油耗、计价等数据进行计算,有一辆车花费了108.1元,还有一辆车的花费是29.2元,当计算这两个价格的和时出问题了,结果竟然不是137.3,而是137.29999999999998!

@Test
public void test() {
    Double d = 108.1;
    Double dd = 29.2;
    System.out.println("108.1 + 29.2 = " + (d + dd));
}

结果:

108.1 + 29.2 = 137.29999999999998

当时我是不慌的,出现这种问题一般就是和定义的数据类型有关,一开始我们定义里程、油耗和价格等数据指标时,全部用Double定义的,问题就出现在这里!

问题分析

上面我猜是因为Double类型引起的,再来用一个简单的0.1 + 0.2看看等不等于0.3:

@Test
public void test() {
    double d1 = 0.1;
    double d2 = 0.2;
    double d3 = d1 + d2;
    System.out.println("double d1 + d2 = " + d3);
}

结果:

double d1 + d2 = 0.30000000000000004

那么为什么程序计算的 0.1 + 0.2不等于0.3呢?

计算机内部是用来存储和处理数据的。用一个二进制串表示数据,十进制转换成二进制,二进制转换成十进制的方法是:

  • 十进制转二进制:除2取余
  • 二进制转十进制:乘2取整

那么,十进制的0.1转成二进制:

由此可知,0.1的二进制表示将会是0.0001100011...

但是计算机是不会允许它一直循环下去的,否则内存会爆掉的。

计算机会在某个精度点直接舍弃剩下的位数,所以,小数0.1在计算机内部存储的并不是精确的十进制的0.1,而是有误差的。

也就是说,二进制无法精确表示大部分的十进制小数

为什么说大部分的十进制小数呢,因为像0.5这样分母是2的倍数的十进制数是没有舍入误差的,计算机能够用二进制精确表示。

优雅的避坑

方式1 货币类字段精确到分用long类型表示

使用long类型来表示价格,当然价格精确到分

那么开篇提到的两个价格计算,108.1元=108.1 * 10 * 10分=10810分,29.2元=29.2 * 10 * 10分=2920分,求和:

@Test
public void testLong() {
    long l1 = 10810;
    long l2 = 2920;
    System.out.println("l1 + l2 = " + (l1 + l2));
}

结果:

l1 + l2 = 13730

这样计算出价格是以为单位的,显示的时候转成或者其他需要的单位即可。

方式2 用BigDecimal进行运算

还有一种方式就是用BigDecimalString结合,构造出BigDecimal对象进行计算:

public BigDecimal(String val) {
    this(val.toCharArray(), 0, val.length());
}

因为BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常,因此:

优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。

BigDecimal构造方法
BigDecimal构造方法
@Test
public void testBigDecimal() {
    BigDecimal bd1 = new BigDecimal("108.1");
    BigDecimal bd2 = new BigDecimal("29.2");
    System.out.println("BigDecimal bd1与bd2的和:" + bd1.add(bd2));
}

结果:

BigDecimal bd1与bd2的和:137.3

小结

用阿里Java开发手册中提到的以下几点作为总结:

  • 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。
  • 【强制】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。

说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。

  • 【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。

说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。

优先推荐入参为 String 的构造方法,或使用 BigDecimalvalueOf 方法,此方法内部其实执行了 DoubletoString,而 DoubletoStringdouble 的实际能表达的精度对尾数进行了截断。

关注我,持续与您分享

欢迎关注微信公众号:行百里er,回复 java 可获得【避坑】系列pdf文档,以及精品Java电子书资料:

- END -