说一说 BigDecimal 的五个坑

简介: 我是小假 期待与你的下一次相遇 ~
  1. 1、BigDecimal中的五个容易踩的坑
    1.1 new BigDecimal()还是BigDecimal#valueOf()?
    先看下面这段代码
  1. BigDecimal bd1 = new BigDecimal(0.01);
  2. BigDecimal bd2 = new BigDecimal("0.01");
  3. System.out.println("bd1 = " + bd1);
  4. System.out.println("bd2 = " + bd2);
  1. 输出到控制台的结果是:
  1. bd1 = 0.01000000000000000020816681711721685132943093776702880859375
  2. bd2 = 0.01
  1. 造成这种差异的原因是0.1这个数字计算机是无法精确表示的,送给BigDecimal的时候就已经丢精度了,而BigDecimal#valueOf的实现却完全不同
  1. public static BigDecimal valueOf(double val) {
  2. // Reminder: a zero double returns '0.0', so we cannot fastpath
  3. // to use the constant ZERO.  This might be important enough to
  4. // justify a factory approach, a cache, or a few private
  5. // constants, later.
  6. return new BigDecimal(Double.toString(val));
  7. }
  1. 它使用了浮点数相应的字符串来构造BigDecimal对象,因此避免了精度问题。所以大家要尽量要使用字符串而不是浮点数去构造BigDecimal对象,如果实在不行,就使用BigDecimal#valueOf()方法吧。 1.2 等值比较
  1. BigDecimal bd1 = new BigDecimal("1.0");
  2. BigDecimal bd2 = new BigDecimal("1.00");
  3. System.out.println(bd1.equals(bd2));
  4. System.out.println(bd1.compareTo(bd2));
  1. 控制台的输出将会是:
  1. false
  2. 0
  1. 究其原因是,BigDecimal中equals方法的实现会比较两个数字的精度,而compareTo方法则只会比较数值的大小。 1.3 BigDecimal并不代表无限精度
    先看这段代码
  1. BigDecimal a = new BigDecimal("1.0");
  2. BigDecimal b = new BigDecimal("3.0");
  3. a.divide(b) // results in the following exception.
  1. 结果会抛出异常:
  1. java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
  1. 关于这个异常,Oracle的官方文档有具体说明

If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

大意是,如果除法的商的结果是一个无限小数但是期望返回精确的结果,那程序就会抛出异常。回到这个例子,需要告诉JVM不需要返回精确的结果就好了

  1. BigDecimal a = new BigDecimal("1.0");
  2. BigDecimal b = new BigDecimal("3.0");
  3. a.divide(b, 2, RoundingMode.HALF_UP)// 0.33

1.4 BigDecimal转回String要小心

  1. BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
  2. String out = d.toString(); // Or perform any formatting that needs to be done
  3. System.out.println(out); // 1.23345353454567E+16

可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果BigDecimal有三个方法可以转为相应的字符串类型,切记不要用错:

  1. String toString();     // 有必要时使用科学计数法
  2. String toPlainString();   // 不使用科学计数法
  3. String toEngineeringString();  // 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数

1.5 执行顺序不能调换(乘法交换律失效)

乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况

  1. BigDecimal a = BigDecimal.valueOf(1.0);
  2. BigDecimal b = BigDecimal.valueOf(3.0);
  3. BigDecimal c = BigDecimal.valueOf(3.0);
  4. System.out.println(a.divide(b, 2, RoundingMode.HALF_UP).multiply(c)); // 0.990
  5. System.out.println(a.multiply(c).divide(b, 2, RoundingMode.HALF_UP)); // 1.00

别小看这这0.01的差别,在汇金领域,会产生非常大的金额差异。

2、最佳实践

关于金额计算,很多业务团队会基于BigDecimal再封装一个Money类,其实直接可以用一个半官方的Money类:JSR 354 ,虽然没能在Java 9中成为Java标准,很有可能集成到后续的Java版本中成为官方库。

2.1 maven坐标

  1. <dependency>
  2.  <groupId>org.javamoney</groupId>
  3.  <artifactId>moneta</artifactId>
  4.  <version>1.1</version>
  5. </dependency>

2.2 新建Money类

  1. CurrencyUnit cny = Monetary.getCurrency("CNY");
  2. Money money = Money.of(1.0, cny);
  3. // 或者 Money money = Money.of(1.0, "CNY");
  4. //System.out.println(money);

2.3 金额运算

  1. CurrencyUnit cny = Monetary.getCurrency("CNY");
  2. Money oneYuan = Money.of(1.0, cny);
  3. Money threeYuan = oneYuan.add(Money.of(2.0, "CNY")); //CNY 3
  4. Money tenYuan = oneYuan.multiply(10); // CNY 10
  5. Money fiveFen = oneYuan.divide(2); //CNY 0.5

2.4 比较相等

  1. Money fiveFen = Money.of(0.5, "CNY"); //CNY 0.5
  2. Money anotherFiveFen = Money.of(0.50, "CNY"); // CNY 0.50
  3. System.out.println(fiveFen.equals(anotherFiveFen)); // true

可以看到,这个类对金额做了显性的抽象,增加了金额的单位,也避免了直接使用BigDecimal的一些坑。


相关文章
|
JSON 算法 安全
不破不立!Fastjson2.0 性能炸裂,为了下一个十年
Alibaba Fastjson: 目前在人类已知范围内,这个星球跑的最快的Java JSON库。在过去的十年里,fastjson v1作为国内github star最多和最受欢迎的json解析库,如今fastjson v2 重磅来袭,性能炸裂。
19944 2
不破不立!Fastjson2.0 性能炸裂,为了下一个十年
|
人工智能 Java Spring
Spring Boot循环依赖的症状和解决方案
Spring Boot循环依赖的症状和解决方案
|
Oracle Java 关系型数据库
Oracle jdk 的国内下载镜像
Oracle jdk 的国内下载镜像
63358 0
|
SQL Java 数据库连接
Mybatis的批处理工具:MybatisBatchUtils功能全解
总而言之,MybatisBatchUtils 是 Mybatis 的一款强大工具,可以显著提高批量数据处理的效率,并确保事务的安全性。通过简化 API 的设计,使得开发者能够易于上手并利用 Mybatis 进行高效的数据库操作。正确使用 MybatisBatchUtils,必然能够在大数据量的场景下,给你的应用性能带来质的飞跃。
732 0
|
程序员
【工具使用】Intellij IDEA 自动清除无效 import 包 和 清除无效 import包 的快捷键
【工具使用】Intellij IDEA 自动清除无效 import 包 和 清除无效 import包 的快捷键
6319 0
|
存储 安全 Java
深入理解 Java 中的 instanceof 关键字
本文深入解析了 Java 中的 `instanceof` 关键字,探讨其在类型判断中的作用。作为二元操作符,`instanceof` 可用于检查对象是否为某类实例或实现特定接口,避免类型转换异常 (`ClassCastException`)。文章通过多态性下的类型判断、安全类型转换、接口实现检测及集合元素类型判定等实际应用场景,展示了 `instanceof` 的强大功能。掌握该关键字可提高代码健壮性,确保运行时类型安全。
1287 0
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
1890 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
前端开发 网络协议
netty整合websocket(完美教程)
本文是一篇完整的Netty整合WebSocket的教程,介绍了WebSocket的基本概念、使用Netty构建WebSocket服务器的步骤和代码示例,以及如何创建前端WebSocket客户端进行通信的示例。
2516 2
netty整合websocket(完美教程)
|
Prometheus 监控 Cloud Native
无痛入门Prometheus:一个强大的开源监控和告警系统,如何快速安装和使用?
Prometheus 是一个完全开源的系统监控和告警工具包,受 Google 内部 BorgMon 系统启发,自2012年由前 Google 工程师在 SoundCloud 开发以来,已被众多公司采用。它拥有活跃的开发者和用户社区,现为独立开源项目,并于2016年加入云原生计算基金会(CNCF)。Prometheus 的主要特点包括多维数据模型、灵活的查询语言 PromQL、不依赖分布式存储、通过 HTTP 拉取时间序列数据等。其架构简单且功能强大,支持多种图形和仪表盘展示模式。安装和使用 Prometheus 非常简便,可以通过 Docker 快速部署,并与 Grafana 等可
6590 3

热门文章

最新文章