【优雅的避坑】为什么0.1+0.2不等于0.3了!?
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
# 问题初现
我碰到过这样一个问题,对项目上用车记录中的用车里程、油耗、计价等数据进行计算,有一辆车花费了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));
}
2
3
4
5
6
结果:
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);
}
2
3
4
5
6
7
结果:
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));
}
2
3
4
5
6
结果:
l1 + l2 = 13730
这样计算出价格是以分为单位的,显示的时候转成元或者其他需要的单位即可。
# 方式2 用BigDecimal进行运算
还有一种方式就是用BigDecimal
和String
结合,构造出BigDecimal对象进行计算:
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
2
3
因为BigDecimal(double)
存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常,因此:
优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
@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));
}
2
3
4
5
6
结果:
BigDecimal bd1与bd2的和:137.3
# 小结
用阿里Java开发手册中提到的以下几点作为总结:
- 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。
- 【强制】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用
equals
来判断。
说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。
- 【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。
说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
优先推荐入参为
String
的构造方法,或使用BigDecimal
的valueOf
方法,此方法内部其实执行了Double
的toString
,而Double
的toString
按double
的实际能表达的精度对尾数进行了截断。