行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • JVM

  • Java基础

    • 【优雅的避坑】从验证码生成代码的优化到 JVM 栈和堆
    • 【优雅的避坑】HashMap正确的避免扩容带来的额外开销
    • 【优雅的避坑】不要轻易使用==比较两个Integer的值
    • 【优雅的避坑】为什么0.1+0.2不等于0.3了!?
      • 问题初现
      • 问题分析
      • 优雅的避坑
        • 方式1 货币类字段精确到分用long类型表示
        • 方式2 用BigDecimal进行运算
      • 小结
      • 关注我,持续与您分享
    • 【优雅的避坑】不安全!别再共享SimpleDateFormat了
    • new Object在内存中占多少字节?
    • Java 8之Lambda表达式的写法套路
    • Java 8 Stream API可以怎么玩?
    • Java最强大的技术之一:反射
    • 从一道面试题进入Java并发新机制---JUC
  • Java
  • Java基础
行百里er
2020-09-28
目录

【优雅的避坑】为什么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));
}
1
2
3
4
5
6

结果:

108.1 + 29.2 = 137.29999999999998
1

不慌

当时我是不慌的,出现这种问题一般就是和定义的数据类型有关,一开始我们定义里程、油耗和价格等数据指标时,全部用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);
}
1
2
3
4
5
6
7

结果:

double d1 + d2 = 0.30000000000000004
1

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

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

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

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

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));
}
1
2
3
4
5
6

结果:

l1 + l2 = 13730
1

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

# 方式2 用BigDecimal进行运算

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

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

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

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

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));
}
1
2
3
4
5
6

结果:

BigDecimal bd1与bd2的和:137.3
1

# 小结

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

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

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

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

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

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

# 关注我,持续与您分享

二维码

#JVM
上次更新: 2022/10/04, 18:14:30
【优雅的避坑】不要轻易使用==比较两个Integer的值
【优雅的避坑】不安全!别再共享SimpleDateFormat了

← 【优雅的避坑】不要轻易使用==比较两个Integer的值 【优雅的避坑】不安全!别再共享SimpleDateFormat了→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式