众所周知,double 和 float 这些浮点数其实是不精确的。

比如 0.1 + 0.2 并不等于 0.3,而是等于 0.30000000000000004——这也一度成为程序员圈子里的经典梗。所以用浮点数表示金额这种需要精确计算的数值,是会出现精度丢失问题的。

double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 输出: 0.30000000000000004
System.out.println(a + b == 0.3); // 输出: false

再看一个更实际的例子,假设你在做一个电商系统的金额计算:

double price = 2.0;
double discount = 0.9;
System.out.println(price * discount); // 输出: 1.7999999999999998

你看,原本应该是 1.8 的结果,却变成了 1.7999999999999998。如果这是真实的订单金额,那可就出大问题了

为什么会精度丢失

为什么会有这种精度丢失呢?因为计算机底层都是用二进制存储的,但并不是所有十进制数都能用二进制精确表示。各位有兴趣的话可以试着算一下 0.1 的二进制是多少,算出来可以在评论区分享一下。

算了一会你可能会发现:这怎么算不完?没错,出现了无限循环的情况——(0.1)₁₀ = (0.000110011001100...)₂ 像这种情况,计算机就没办法用二进制精确表示 0.1 了。

而 double 类型在 Java 中占 64 位,按照 IEEE 754 标准,其中 1 位是符号位,11 位是指数位,52 位是尾数位。当遇到无限循环的二进制小数时,只能截断保存,这就导致了精度丢失。

BigDecimal

在 Java 中,无论是单精度还是双精度,表示的都是近似值。

为了表示精确的小数值,Java 提供了 BigDecimal 类型。BigDecimal 由两个部分组成:无标度值(unscaled value)和标度(scale)。无标度值是一个整数,表示实际的数值;标度也是一个整数,表示小数点后的位数。

举个例子,数字 123.45 在 BigDecimal 中:

  • 无标度值是 12345
  • 标度是 2

实际值就是:12345 × 10⁻² = 123.45

用 BigDecimal 来处理刚才的金额计算:

BigDecimal price = new BigDecimal("2.0");
BigDecimal discount = new BigDecimal("0.9");
BigDecimal result = price.multiply(discount);
System.out.println(result); // 输出: 1.80

这下结果就对了

equals 的坑

在 BigDecimal 中不能用 equals 方法做等值比较,因为 equals 会同时比较无标度值和标度这两个内容。

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.equals(b)); // 输出: false

我们都知道 0.1 和 0.10 在数值上是相等的,但 equals 的结果却是 false。这是因为:

  • a 的无标度值是 1,标度是 1
  • b 的无标度值是 10,标度是 2

虽然值相同,但它们的标度不同,所以 equals 返回 false。

compareTo

比较 BigDecimal 大小时应该使用 compareTo 方法,返回值为 1、-1、0,分别代表大于、小于、等于。

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.compareTo(b)); // 输出: 0,表示相等 BigDecimal c = new BigDecimal("0.2");
System.out.println(a.compareTo(c)); // 输出: -1,表示 a < c
System.out.println(c.compareTo(a)); // 输出: 1,表示 c > a

创建 BigDecimal 的正确姿势

创建 BigDecimal 时,建议使用 String 类型的构造方法,也就是 new BigDecimal("0.1") 这样。

BigDecimal right = new BigDecimal("0.1");
System.out.println(right); // 输出: 0.1 BigDecimal wrong = new BigDecimal(0.1);
System.out.println(wrong); // 输出: 0.1000000000000000055511151231257827021181583404541015625

如果你用了 new BigDecimal(0.1) 的方式,创建出来的值其实也不是 0.1,而是一个近似值。这是因为传入的 double 本身就已经是近似值了,BigDecimal 只是忠实地把这个近似值保存下来而已。

还有一个更方便的方法:
BigDecimal bd = BigDecimal.valueOf(0.1);
System.out.println(bd); // 输出: 0.1

valueOf 方法内部会先把 double 转成 String,再调用 String 构造方法,所以也是安全的。

常用的 BigDecimal 运算

BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("2.3"); // 加法
System.out.println(a.add(b)); // 12.8 // 减法
System.out.println(a.subtract(b)); // 8.2 // 乘法
System.out.println(a.multiply(b)); // 24.15 // 除法(需要指定精度和舍入模式)
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP)); // 4.57

注意除法操作时,如果不指定精度,遇到除不尽的情况会抛出 ArithmeticException。所以建议都加上精度和舍入模式。

总之,涉及金额计算时,千万别图省事用 double,老老实实用 BigDecimal 才是王道。

为什么不该用 Double 表示金额及解决方案的更多相关文章

  1. Data truncation: Truncated incorrect DOUBLE value错误的解决方案

    Data truncation: Truncated incorrect DOUBLE value错误的解决方案: 当在修改某条单位记录时,发生了Data truncation: Truncated ...

  2. MyBatis调用存储过程

    MySQL存储过程 DROP PROCEDURE IF EXISTS transferMoney; -- 实现转账功能的存储过程 CREATE PROCEDURE transferMoney ( IN ...

  3. c++课程实训 银行储蓄系统

    基本要求:定义了用户类(User)和银行类(Bank),用成员函数实现各种功能,多文件组织程序.能用文本文件存取数据(如演示样例中给出的技术): 拓展方向: 序号 加分项目 细       则 1 改 ...

  4. Qt 学习之路 :自定义只读模型

    model/view 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListView.QTableView和QTreeView提供一个数据模型,这样我们可以从不同角度来展示数据的方方面面 ...

  5. Hive学习笔记【转载】

    本文转载自:http://blog.csdn.net/haojun186/article/details/7977565 1.  HIVE结构 Hive 是建立在 Hadoop 上的数据仓库基础构架. ...

  6. Hibernate中事务小案例

    理论知识: 什么是事务? 指作为单个逻辑工作单位执行的一系列操作,要么完全的执行,要么完全不执行.事务处理可以确保非事务性单元内的所有操作都完全完成,否则永久不会更新面向数据的资源.通过将一组操作组合 ...

  7. mysql系列一、mysql数据库规范

    一. 表设计 库名.表名.字段名必须使用小写字母,“_”分割. 库名.表名.字段名必须不超过12个字符. 库名.表名.字段名见名知意,建议使用名词而不是动词. 表必须使用InnoDB存储引擎. 表必须 ...

  8. Disrunptor多生产者多消费者模型讲解

    多生产者多消费者模拟需求:1.创建100个订单生产者,每个生产者生产100条订单,总共会生产10000条订单,由3个消费者进行订单消费处理.2.100个订单生产者全部创建完毕,再一起生产消费订单数据 ...

  9. EventProcessor与WorkPool用法--可处理多消费者

    单一的生产者,消费者有多个,使用WorkerPool来管理多个消费者: RingBuffer在生产Sequencer中记录一个cursor,追踪生产者生产到的最新位置,通过WorkSequence和s ...

  10. C#基础第八天-作业-设计类-面向对象方式实现两个帐户之间转账

    要求1:完成以下两种账户类型的编码.银行的客户分为两大类:储蓄账户(SavingAccount)和信用账户(CreditAccount),两种的账户类型的区别在于:储蓄账户不允许透支,而信用账户可以透 ...

随机推荐

  1. virtual studio 未找到pdb 解决方案

    参照 百度知道

  2. Semantic Kernel Agent Orchestration编排

    一.多代理编排核心价值 Semantic Kernel的Agent Orchestration框架解决了传统单代理系统的局限性: // 统一调用接口示例(适用于所有模式) InProcessRunti ...

  3. Solon Flow v3.4.0 轻量级流程编排框架

    Solon Flow 是一个轻量级流程编排框架(采用 yaml 或 json 偏平式编排格式) 支持无状态流程 可用于计算(或任务)的编排场景 可用于业务规则和决策处理型的编排场景 支持有状态流程 可 ...

  4. win11 64位纯净版如何自动隐藏任务栏的问题

    有很多雨林木风官网的用户可能都不知道使用的win11 64位纯净版系统,可以设置win11自动隐藏任务栏.而且只要鼠标放到任务栏位置,它就会自动出现,也是一个不错的功能.那么我们要如何设置呢?本文中, ...

  5. SQLcl:不仅是 SQL*Plus 的继任者,更是 AI 时代的连接器

    在 Oracle 数据库的世界里,SQL*Plus 是开发和管理工作的标配工具.但随着数据库技术的演进和 AI 能力的嵌入,传统工具已无法满足现代数据开发.智能交互和自动化运维的需求. Oracle ...

  6. GitOps:云原生时代的革命性基础设施管理范式

    在数字化转型浪潮席卷全球的当下,云原生技术已成为企业构建现代化应用的事实标准.然而,随着应用复杂度的指数级增长,传统基础设施管理方式正面临前所未有的挑战.GitOps作为一种颠覆性的管理理念,正在重塑 ...

  7. 深度对比 Coze 与 Dify,一文看懂如何选型

    大家好,我是汤师爷,专注AI智能体分享,致力于帮助100W人用智能体创富~ 随着Coze开源,业内许多人认为它可能对Dify构成威胁. 本文从架构设计.技术栈和适用场景等方面对这两个平台进行全面对比. ...

  8. C++ 使用分治减小模板递归深度

    起因 C++14 引入 STL 的 make_index_sequence 可以生成一个类型为 std::size_t,0 到 N-1 的编译期序列,我们可以这样使用它: 代码 //利用函数参数推导提 ...

  9. Linux 系统修改 open files 无效

    原因也许你通过各种方式,知道通过以下方式可以修改 open files: $ vi /etc/security/limits.conf* soft nofile 65535* hard nofile ...

  10. 【Docker】通过Docker部署BookStack

    为了给公司搭建个知识库重新做了选型,看过了好几个工具包括MkDocs.MM-markdown等,最后选用开源的BookStack作为知识库,原因有三: BookStack是开源的,并且项目在GitHu ...