说一说 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. 究其原因是,BigDecimalequals方法的实现会比较两个数字的精度,而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的一些坑。


相关文章
|
1月前
|
XML 数据可视化 Java
|
17天前
|
XML 存储 前端开发
免费在线图片转Base64编码工具
利用图片转 Base64 编码工具(支持 PNG, GIF, JPEG 等格式)。操作极其简单:将目标图片拖拽至工具指定区域,即可自动完成转换并获得编码结果。
622 3
|
27天前
|
Java API 微服务
2025 年 Java 从入门到精通学习笔记全新版
《Java学习笔记:从入门到精通(2025更新版)》是一本全面覆盖Java开发核心技能的指南,适合零基础到高级开发者。内容包括Java基础(如开发环境配置、核心语法增强)、面向对象编程(密封类、接口增强)、进阶技术(虚拟线程、结构化并发、向量API)、实用类库与框架(HTTP客户端、Spring Boot)、微服务与云原生(容器化、Kubernetes)、响应式编程(Reactor、WebFlux)、函数式编程(Stream API)、测试技术(JUnit 5、Mockito)、数据持久化(JPA、R2DBC)以及实战项目(Todo应用)。
97 5
|
3月前
|
开发框架 人工智能 Java
破茧成蝶:传统J2EE应用无缝升级AI原生
本文探讨了技术挑战和解决方案,还提供了具体的实施步骤,旨在帮助企业顺利实现从传统应用到智能应用的过渡。
破茧成蝶:传统J2EE应用无缝升级AI原生
|
2月前
|
Java 程序员 应用服务中间件
【高薪程序员必看】万字长文拆解Java并发编程!(2 2-2)
📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!💡 独家亮点抢先看:✅ 图解JVM内存模型(JMM)三大特性,看完再也不怕指令重排序✅ 手撕ReentrantLock源码,AQS队列同步器实现原理大揭秘✅ 全网最细线程状态转换图(附6种状态转换触发条件表)
65 0
|
29天前
|
XML 人工智能 Java
注入Java Bean的方式
本文总结了 Spring Boot 中常见的 Bean 注入方式,包括字段注入(`@Autowired`)、构造器注入(推荐)、Setter 方法注入、`@Resource` 按名称注入、`@Bean` 定义复杂 Bean、`@Value` 注入配置值、XML 配置、`ApplicationContextAware` 手动获取 Bean 以及 JSR-330 的 `@Inject`。同时分析了 Setter 注入逐渐被淡化的原因,强调构造器注入的不可变性和安全性优势,并推荐结合 Lombok 简化代码。文章还提供了按需选择注解的建议和最佳实践,帮助开发者根据场景选择合适的依赖注入方式。
88 49
|
28天前
|
机器学习/深度学习 人工智能 前端开发
AI+Code驱动的M站首页重构实践:从技术债务到智能化开发
本文分享了阿里巴巴找品M站首页重构项目中AI+Code提效的实践经验。面对M站技术栈陈旧、开发效率低下的挑战,我们通过楼层动态化架构重构和AI智能脚手架,实现了70%首页场景的标准化覆盖 + 30%的非标场景的研发提速,开发效率分别提升90%+与40%+。文章详细介绍了楼层模板沉淀、AI辅助代码生成、智能组件复用评估等核心实践,为团队AI工程能力升级提供了可复制的方法论。
195 15
AI+Code驱动的M站首页重构实践:从技术债务到智能化开发
|
20天前
|
Java 测试技术 API
我们来说说如何使用 Lambda 表达式实现排序功能
我是小假 期待与你的下一次相遇 ~
|
28天前
|
XML Java API
说一说 @Autowired 注解实现原理
我是小假 期待与你的下一次相遇 ~