Java开发篇- 从BigDecimal的divide的异常说起

简介: 在过去做项目的某一天中,突然有小伙伴说两个BigDecimal的数据相除(divide)报错了,觉得不可能,然后问他是怎么编写的,他说很简单呀,就是new了2个BigDecimal,然后相除的结果赋值给另外一个BigDecimal对象。听起来觉得没有问题,正常来说,2个Integer(int),2个Double(double)都不会报错,然后问是什么异常,说是一个很奇怪的异常

开发小技巧系列文章,是本人对过往平台系统的设计开发及踩坑的记录与总结,给初入平台系统开发的开发人员提供参考与帮助

在过去做项目的某一天中,突然有小伙伴说两个BigDecimal的数据相除(divide)报错了,觉得不可能,然后问他是怎么编写的,他说很简单呀,就是new了2个BigDecimal,然后相除的结果赋值给另外一个BigDecimal对象。听起来觉得没有问题,正常来说,2个Integer(int),2个Double(double)都不会报错,然后问是什么异常,说是一个很奇怪的异常。

问不出情况,直接看代码吧(省去了一些其他无关的代码)

//省略业务的逻辑 ...

//得到订单金额
BigDecimal orderAmount = new BigDecimal(2312.23);
//得到成本
BigDecimal cost = new BigDecimal(984.23);
//得到客户数
BigDecimal customerCount = new BigDecimal(35);
//得到客单价
BigDecimal pricePerCustomer = orderAmount.divide(customerCount);
log.info("客单价:{}", pricePerCustomer);

//得到毛利率
BigDecimal profit = (orderAmount.subtract(cost)).divide(cost);
log.info("毛利率:{}", profit);

//省去其他逻辑...
AI 代码解读

代码看起来没什么问题(一般的小伙伴),为什么会报错呢?来看下异常信息,如下:

11:01:10.027 [main] INFO net.jhelp.demo.BigDecimalTest - 客单价:66.06371428571428623399697244167327880859375
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide(BigDecimal.java:1690)
AI 代码解读

异常的信息是:

算术运算异常:相除结果为无限小数;没有可精确表示的计算结果。

从异常中可以看出是“计算结果除不尽,没办法确定相除的结果”,从这个信息的提示上看,是要给BigDecimal的“商”设置计算结果的“小数位”。

但是很奇怪,上面的程序中有2处用了divide方法,一处没有报错,一处报错,也就是用divide方法不一定会抛出上面的异常,带上面的问题,来调试一下程序,找到BigDecimal的divide方法在里设置一个断点,当执行到第一个divide时的调试如下:

image.png

从上面可以看到,在代码里面直接new BigDecimal(2312.23),实际上在程序运行时,它变成了一个带41位小数的浮点数(scale=41)(题外话:在构造确实数值时,还是使用new BigDecimal("2312.23"),这样出来就不会带过的小数位),另外一个数值为new BigDecimal(35),就是35的值,精度为0(scale=0)。图上还看到一个属性(intCompact),有不同的值(-9223372036854775808, 35).

new BigDecimal(2312.23)实际上调用的是BigDecimal中的浮点数构造方法:

public BigDecimal(Double value){
   
   
   .....   
   this.intVal = intVal;
   this.intCompact = compactVal;
   this.scale = scale;
   this.precision = prec;
}
AI 代码解读

而new BigDecimal(35)调用的是BigDecimal中的整数构造方法:

public BigDecimal(int val) {
   
   
        this.intCompact = val;
        this.scale = 0;
        this.intVal = null;
    }
AI 代码解读

再继续往下走,来看看preferredScale(预期的小数位精度)是多少。

image.png

这时的preferredScale=41, 继续往下看。

image.png

image.png

image.png

从上图中看到,BigDecimal在计算2个除数的商时,如果被除数是整数,那商的精度由除数的精度决定(从计算preferredScale可以看出)。另外,还生成了一个MathContext的对象,默认进位规则是:RoundingMode.UNNECESSARY。

这里面有一个重要的分支
image.png

当BigDecimal是一个浮点类型的数时,它的intCompact是和INFLATED的值相同的。

方法的调用流程是这样:

image.png

最终返回一个跟“除数”一样精度的“商”值。

来看第二个divide的跟踪过程

image.png

image.png

第二个divide的除数,被除数的scale=41, intCompact的值相同(-9223372036854775808==INFLATED),preferredScale=0,而这时的MathContext 与前一个有很大的不同,它的precision=192.

image.png

this.precision() = 45
(long)Math.ceil(10.0*divisor.precision()/3.0 = 147
AI 代码解读

而在这个逻辑分支时,2个浮点相除的,走的是下面的方法,和第一个计算时走的分支是不同的,第一个计算走的是上面的方法。

image.png

来看看它的方法调用流程:
image.png

image.png

/**
     * Shared logic of need increment computation
.     */
    private static boolean commonNeedIncrement(int roundingMode, int qsign,                                     int cmpFracHalf, boolean oddQuot) {
   
   

        switch(roundingMode) {
   
   
        case ROUND_UNNECESSARY:
            throw new ArithmeticException("Rounding necessary");
        case ROUND_UP: // Away from zero
            return true;
        case ROUND_DOWN: // Towards zero
            return false;
         //略...
  }
AI 代码解读

跟踪到这里,是不是清晰很多了,这里的roundingMode就是在调用divide的方法中获取的,也是是前面的MathContext对象生成时,默认设置的RoundingMode.UNNECESSARY,从这个方法的调用过程中,可以看出divide(BigDecimal divisor)方法不适合用于2个数都是“浮点数“的场景的相除。(题外话,这里抛出的异常,与外面见到的异常是不一样的,有点坑,为什么这个方法不标志一下@Deprecated,让别人不要掉坑里。)

总结

上面根据异常的信息,对BigDecimal的divide(除法)的源代码分析与跟踪,分析出现问题的原因,在开发的过程中,不要使用divide(BigDecimal divisor)这方法,以免掉到坑里,尽理使用divide(BigDecimal divisor, int scale, int roundingMode)这种,指定精度,和“舍入规则”。

文字点滴,希望你能收获点滴,觉得不错的可以点个赞。

开发小技巧系列文章:

1、开发小技巧系列 - 库存超卖,库存扣成负数?
2、开发小技巧系列 - 重复生成订单
3、开发小技巧系统 - Java实现树形结构的方式有那些?
4、开发小技巧系列 - 如何避免NullPointerException?(一)
5、开发小技巧系列 - 如何避免NullPointerException?(二)
6.开发小技巧系列 - 如何避免NPE,巧用Optional重构三元表达式?(三)

7.开发小技巧系列 - 如何避免NPE,去掉if...else(四)

目录
打赏
0
0
0
0
392
分享
相关文章
一天成为Java开发高手:用飞算JavaAI实现十倍提效
“一天成为Java开发高手”曾被视为天方夜谭,但飞算JavaAI的出现改变了这一局面。这款AI开发助手通过智能引导、需求分析、自动化逻辑处理和完整代码工程生成,大幅简化了Java开发流程。它不仅帮助新手快速上手,还让资深开发者提高效率,减少调试时间。现在,参与“飞算JavaAI炫技赛”,展示你的开发实力,赢取丰厚奖品!
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
87 14
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
45 12
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
92 5
Java高级应用开发:基于AI的微服务架构优化与性能调优
在现代企业级应用开发中,微服务架构虽带来灵活性和可扩展性,但也增加了系统复杂性和性能瓶颈。本文探讨如何利用AI技术,特别是像DeepSeek这样的智能工具,优化Java微服务架构。AI通过智能分析系统运行数据,自动识别并解决性能瓶颈,优化服务拆分、通信方式及资源管理,实现高效性能调优,助力开发者设计更合理的微服务架构,迎接未来智能化开发的新时代。
菜鸟之路day02-04拼图小游戏开发一一JAVA基础综合项目
本项目基于黑马程序员教程,涵盖面向对象进阶、继承、多态等知识,历时约24小时完成。项目去除了登录和注册模块,专注于单机游戏体验。使用Git进行版本管理,代码托管于Gitee。项目包含窗体搭建、事件监听、图片加载与打乱、交互逻辑实现、菜单功能及美化界面等内容。通过此项目,巩固了Java基础并提升了实际开发能力。 仓库地址:[https://gitee.com/zhang-tenglan/puzzlegame.git](https://gitee.com/zhang-tenglan/puzzlegame.git)
46 6
【潜意识Java】javaee中的SpringBoot在Java 开发中的应用与详细分析
本文介绍了 Spring Boot 的核心概念和使用场景,并通过一个实战项目演示了如何构建一个简单的 RESTful API。
50 5
|
10月前
|
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
158 0
|
10月前
|
你知道Java中的BigInteger类和BigDecimal类吗?
你知道Java中的BigInteger类和BigDecimal类吗?
155 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等