• 周四. 6月 30th, 2022

5G编程聚合网

5G时代下一个聚合的编程学习网

热门标签

Java-BigDecimal踩坑记录

admin

11月 28, 2021

1.为什么要用BigDecimal?

浮点数的计算过程中必然会造成精度丢失,BigDecimal丢失程度比float和double小。

float和double是基本数据类型,而BigDecimal是封装类型。

有得必有失,BigDecimal耗费时间和空间换取精度准确。

2.初始化就存在精度问题

double price = 18.899;
BigDecimal a = BigDecimal.valueOf(price);
BigDecimal b = new BigDecimal(price);
System.out.println(a);//18.899
System.out.println(b);//18.8990000000000009094947017729282379150390625
System.out.println(new BigDecimal(0.85));//0.84999999999999997779553950749686919152736663818359375

显然,建议用BigDecimal.valueOf()方法初始化

3.使用除法divide的时候需要设置取整方式

        BigDecimal e = BigDecimal.valueOf(0.85);
        BigDecimal f = e.divide(BigDecimal.valueOf(0.85)).setScale(4,RoundingMode.DOWN);
        System.out.println(f);//1.0000
        e = BigDecimal.valueOf(1);
        f = e.divide(BigDecimal.valueOf(0.85),BigDecimal.ROUND_DOWN).setScale(4,RoundingMode.DOWN);//divide中不取整会报除不尽异常
        System.out.println(f);//1.0000
        e = BigDecimal.valueOf(1000);
        f = e.divide(BigDecimal.valueOf(0.85),BigDecimal.ROUND_DOWN).setScale(4,RoundingMode.DOWN);//但是取整放大倍数后发现精度也有损失
        System.out.println(f);//1176.0000
        e = BigDecimal.valueOf(1);
        f = NumberUtil.div(e,BigDecimal.valueOf(0.85));
        System.out.println(f);//1.1764705882 NumberUtil默认十位精度,减少精度损失

4.遇到过这样一个例子,九折商品

商品定价从别的系统获取,精确到两位小数

折扣价 = 定价 * 0.9

只有一个price字段用来存储价格,Java中使用BigDecimal,MySQL中使用decimal(10,2)存储。

存的时候存折扣价,使用的时候需要折扣价和定价。

使用折扣价的时候直接用price,使用定价的时候用折扣价去除以0.9。

这样设计肯定是不合理的,这里看一下会造成什么问题。

当商品定价为13.99的时候。(现在商品价格到两位小数也很正常)

        BigDecimal salePrice = BigDecimal.valueOf(13.99);
        BigDecimal rate = BigDecimal.valueOf(0.9);

        BigDecimal price = salePrice.multiply(rate).setScale(2, RoundingMode.DOWN);
        BigDecimal nowPrice = price.divide(rate,BigDecimal.ROUND_DOWN).setScale(2,RoundingMode.DOWN);
        System.out.println(price);//12.59
        System.out.println(nowPrice);//13.98
        //下面的代码没有问题,但是在数据库中是保存两位浮点数,因此数据库存取就是12.59,与上面的一样。
        price = NumberUtil.mul(salePrice,0.9);
        nowPrice = price.divide(rate,BigDecimal.ROUND_DOWN).setScale(2,RoundingMode.DOWN);
        System.out.println(price);//12.591
        System.out.println(nowPrice);//13.99

这就导致了13.98和13.99的精度差的问题,涉及到钱都无小事,目前想到有3种解决方案

(1)再开一列存价格,各玩各的

(2)只存定价,折扣价的使用再去乘

(3)只存折扣价,但是数据库精度取小数点后4位,计算折扣的时候精度不会丢失,除回来也不会丢失

 

5.处理精度的参数RoundingMode roundingMode

RoundingMode就是个枚举,BigDecimal.xxx,本质是数字0-7,代表精度处理方式

        // ROUND_UP() : 有多余的小数位进行进位处理,12.34
        System.out.println(BigDecimal.valueOf(12.333).setScale(2,0));
        // ROUND_DOWN() : 直接去掉多余的小数位,12.33
        System.out.println(BigDecimal.valueOf(12.333).setScale(2,1));
        // ROUND_CEILING(天花板) :正数进位向上,负数舍位向上。12.34 和 -12.33
        System.out.println(BigDecimal.valueOf(12.333).setScale(2,2));
        System.out.println(BigDecimal.valueOf(-12.333).setScale(2,2));
        // ROUND_FLOOR(地板) : 正数舍位向下,负数进位向下 12.33 和 -12.34
        System.out.println(BigDecimal.valueOf(12.333).setScale(2,3));
        System.out.println(BigDecimal.valueOf(-12.333).setScale(2,3));
        // ROUND_HALF_UP(四舍五入)
        System.out.println(BigDecimal.valueOf(12.334).setScale(2,4));
        System.out.println(BigDecimal.valueOf(12.335).setScale(2,4));
        // ROUND_HALF_DOWN(五舍六入)
        System.out.println(BigDecimal.valueOf(12.335).setScale(2,5));
        System.out.println(BigDecimal.valueOf(12.336).setScale(2,5));
        // ROUND_HALF_EVEN(银行家舍入,四舍六入五留双):
        // 这里“四”是指≤4时舍去,"六"是指≥6时进上。"五"指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:5前为奇数,舍5入1;5前为偶数,舍5不进(0是偶数)。
        System.out.println(BigDecimal.valueOf(12.334).setScale(2,6));
        System.out.println(BigDecimal.valueOf(12.336).setScale(2,6));
        System.out.println(BigDecimal.valueOf(12.3351).setScale(2,6));
        System.out.println(BigDecimal.valueOf(12.3550).setScale(2,6));
        System.out.println(BigDecimal.valueOf(12.3450).setScale(2,6));
        // ROUND_UNNECESSARY(非必要舍入):断言请求的操作具有精确的结果,如果对非精确结果的操作指定此舍入模式,则抛出ArithmeticException。
        System.out.println(BigDecimal.valueOf(12.500).setScale(2,7));
        System.out.println(BigDecimal.valueOf(12.5001).setScale(2,7));//异常

0和2,1和3没有试出差别……

 

6.结论

能不用除法就不用除法 

发表评论

您的电子邮箱地址不会被公开。