前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >丸辣!BigDecimal又踩坑了

丸辣!BigDecimal又踩坑了

原创
作者头像
菜菜的后端私房菜
发布2024-08-20 08:59:44
510
发布2024-08-20 08:59:44
举报
文章被收录于专栏:深入浅出Java

丸辣!BigDecimal又踩坑了

前言

小菜之前在国内的一家电商公司自研电商项目,在那个项目中是以人民币的分为最小单位使用Long来进行计算

现在小菜在跨境电商公司也接到了类似的计算需求,在小菜火速完成提交代码后,却被技术leader给叫过去臭骂了一顿

技术leader让小菜将类型改为BigDecimal,小菜苦思不得其解,于是下班后发奋图强,准备搞懂BigDecimal后再对代码进行修改

...

在 Java 中,浮点类型在进行运算时可能会产生精度丢失的问题

尤其是当它们表示非常大或非常小的数,或者需要进行高精度的金融计算时

为了解决这个问题,Java 提供了 BigDecimal

BigDecimal 使用各种字段来满足高精度计算,为了后续的描述,这里只需要记住两个字段

precision字段:存储数据十进制的位数,包括小数部分

scale字段:存储小数的位数

BigDecimal的使用方式再后续踩坑中进行描述,最终总结出BigDecimal的最佳实践

BigDecimal的坑

创建实例的坑

错误示例:

在BigDecimal有参构造使用浮点型,会导致精度丢失

代码语言:java
复制
BigDecimal d1 = new BigDecimal(6.66);

正确的使用方法应该是在有参构造中使用字符串,如果一定要有浮点数则可以使用BigDecimal.valueOf

代码语言:java
复制
private static void createInstance() {
    //错误用法
    BigDecimal d1 = new BigDecimal(6.66);
    
    //正确用法
    BigDecimal d2 = new BigDecimal("6.66");
    BigDecimal d3 = BigDecimal.valueOf(6.66);

    //6.660000000000000142108547152020037174224853515625
    System.out.println(d1);
    //6.66
    System.out.println(d2);
    //6.66
    System.out.println(d3);
}
toString方法的坑

当数据量太大时,使用BigDecimal.valueOf的实例,使用toString方法时会采用科学计数法,导致结果异常

代码语言:java
复制
BigDecimal d2 = BigDecimal.valueOf(123456789012345678901234567890.12345678901234567890);
//1.2345678901234568E+29
System.out.println(d2);

如果要打印正常结果就要使用toPlainString,或者使用字符串进行构造

代码语言:java
复制
private static void toPlainString() {
    BigDecimal d1 = new BigDecimal("123456789012345678901234567890.12345678901234567890");
    BigDecimal d2 = BigDecimal.valueOf(123456789012345678901234567890.12345678901234567890);

    //123456789012345678901234567890.12345678901234567890
    System.out.println(d1);
    //123456789012345678901234567890.12345678901234567890
    System.out.println(d1.toPlainString());

    //1.2345678901234568E+29
    System.out.println(d2);
    //123456789012345678901234567890.12345678901234567890
    System.out.println(d2.toPlainString());
}
比较大小的坑

比较大小常用的方法有equalscompareTo

equals用于判断两个对象是否相等

compareTo比较两个对象大小,结果为0相等、1大于、-1小于

BigDecimal使用equals时,如果两数小数位数scale不相同,那么就会认为它们不相同,而compareTo则不会比较小数精度

代码语言:java
复制
private static void compare() {
    BigDecimal d1 = BigDecimal.valueOf(1);
    BigDecimal d2 = BigDecimal.valueOf(1.00);

    // false
    System.out.println(d1.equals(d2));
    // 0
    System.out.println(d1.compareTo(d2));
}

在BigDecimal的equals方法中能看到,小数位数scale不相等则返回false

代码语言:java
复制
public boolean equals(Object x) {
    if (!(x instanceof BigDecimal))
        return false;
    BigDecimal xDec = (BigDecimal) x;
    if (x == this)
        return true;
    //小数精度不相等 返回 false
    if (scale != xDec.scale)
        return false;
    long s = this.intCompact;
    long xs = xDec.intCompact;
    if (s != INFLATED) {
        if (xs == INFLATED)
            xs = compactValFor(xDec.intVal);
        return xs == s;
    } else if (xs != INFLATED)
        return xs == compactValFor(this.intVal);

    return this.inflated().equals(xDec.inflated());
}

因此,BigDecimal比较时常用compareTo,如果要比较小数精度才使用equals

运算的坑

常见的运算包括加、减、乘、除,如果不了解原理的情况就使用会存在大量的坑

在运算得到结果后,小数位数可能与原始数据发生改变,加、减运算在这种情况下类似

当原始数据为1.00(2位小数位数)和5.555(3位小数位数)相加/减时,结果的小数位数变成3位

代码语言:java
复制
	private static void calc() {
        BigDecimal d1 = BigDecimal.valueOf(1.00);
        BigDecimal d2 = BigDecimal.valueOf(5.555);

        //1.0
        System.out.println(d1);
        //5.555
        System.out.println(d2);
        //6.555
        System.out.println(d1.add(d2));
        //-4.555
        System.out.println(d1.subtract(d2));
    }

在加、减运算的源码中,会选择两数中小数位数(scale)最大的当作结果的小数位数(scale)

代码语言:java
复制
private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
 	//用差值来判断使用哪个scale
    long sdiff = (long) scale1 - scale2;
    if (sdiff == 0) {
        //scale相等时
        return add(xs, ys, scale1);
    } else if (sdiff < 0) {
        int raise = checkScale(xs,-sdiff);
        long scaledX = longMultiplyPowerTen(xs, raise);
        if (scaledX != INFLATED) {
            //scale2大时用scale2
            return add(scaledX, ys, scale2);
        } else {
            BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
            //scale2大时用scale2
            return ((xs^ys)>=0) ? // same sign test
                new BigDecimal(bigsum, INFLATED, scale2, 0)
                : valueOf(bigsum, scale2, 0);
        }
    } else {
        
        int raise = checkScale(ys,sdiff);
        long scaledY = longMultiplyPowerTen(ys, raise);
        if (scaledY != INFLATED) {
            //scale1大用scale1
            return add(xs, scaledY, scale1);
        } else {
            BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
            //scale1大用scale1
            return ((xs^ys)>=0) ?
                new BigDecimal(bigsum, INFLATED, scale1, 0)
                : valueOf(bigsum, scale1, 0);
        }
    }
}

再来看看乘法

原始数据还是1.00(2位小数位数)和5.555(3位小数位数),当进行乘法时得到结果的小数位数为5.5550(4位小数)

代码语言:java
复制
private static void calc() {
    BigDecimal d1 = BigDecimal.valueOf(1.00);
    BigDecimal d2 = BigDecimal.valueOf(5.555);

    //1.0
    System.out.println(d1);
    //5.555
    System.out.println(d2);
    //5.5550
    System.out.println(d1.multiply(d2));
}

实际上1.00会被优化成1.0(上面代码示例的结果也显示了),在进行乘法时会将scale进行相加,因此结果为1+3=4位

代码语言:java
复制
public BigDecimal multiply(BigDecimal multiplicand) {
    //小数位数相加
    int productScale = checkScale((long) scale + multiplicand.scale);
    
    if (this.intCompact != INFLATED) {
        if ((multiplicand.intCompact != INFLATED)) {
            return multiply(this.intCompact, multiplicand.intCompact, productScale);
        } else {
            return multiply(this.intCompact, multiplicand.intVal, productScale);
        }
    } else {
        if ((multiplicand.intCompact != INFLATED)) {
            return multiply(multiplicand.intCompact, this.intVal, productScale);
        } else {
            return multiply(this.intVal, multiplicand.intVal, productScale);
        }
    }
}

而除法没有像前面所说的运算方法有规律性,因此使用除法时必须要指定保留小数位数以及舍入方式

进行除法时可以立马指定保留的小数位数和舍入方式(如代码d5)也可以除完再设置保留小数位数和舍入方式(如代码d3、d4)

代码语言:java
复制
private static void calc() {
    BigDecimal d1 = BigDecimal.valueOf(1.00);
    BigDecimal d2 = BigDecimal.valueOf(5.555);

    BigDecimal d3 = d2.divide(d1);
    BigDecimal d4 = d3.setScale(2, RoundingMode.HALF_UP);
    BigDecimal d5 = d2.divide(d1, 2, RoundingMode.HALF_UP);
    //5.555
    System.out.println(d3);
    //5.56
    System.out.println(d4);
    //5.56
    System.out.println(d5);
}

RoundingMode枚举类提供各种各样的舍入方式,RoundingMode.HALF_UP是常用的四舍五入

除了除法必须指定小数位数和舍入方式外,建议其他运算也主动设置进行兜底,以防意外的情况出现

计算价格的坑

在电商系统中,在订单中会有购买商品的价格明细

比如用完优惠卷后总价为10.00,而买了三件商品,要计算每件商品花费的价格

这种情况下10除3是除不尽的,那我们该如何解决呢?

可以将除不尽的余数加到最后一件商品作为兜底

代码语言:java
复制
private static void priceCalc() {
    //总价
    BigDecimal total = BigDecimal.valueOf(10.00);
    //商品数量
    int num = 3;
    BigDecimal count = BigDecimal.valueOf(num);
    //每件商品价格
    BigDecimal price = total.divide(count, 2, RoundingMode.HALF_UP);
    //3.33
    System.out.println(price);

    //剩余的价格 加到最后一件商品 兜底
    BigDecimal residue = total.subtract(price.multiply(count));
    //最后一件价格
    BigDecimal lastPrice = price.add(residue);
    //3.34
    System.out.println(lastPrice);
}

总结

普通的计算可以以最小金额作为计算单位并且用Long进行计算,而面对汇率、计算量大的场景可以采用BigDecimal作为计算单位

创建BigDecimal有两种常用的方式,字符串作为构造的参数以及浮点型作为静态方法valueOf的参数,后者在数据大/小的情况下toString方法会采用科学计数法,因此最好使用字符串作为构造器参数的方式

BigDecimal比较大小时,如果需要小数位数精度都相同就采用equals方法,忽略小数位数比较可以使用compareTo方法

BigDecimal进行运算时,加减运算会采用原始两个数据中精度最长的作为结果的精度,乘法运算则是将两个数据的精度相加得到结果的精度,而除法没有规律,必须指定小数位数和舍入模式,其他运算方式也建议主动设置小数位数和舍入模式进行兜底

当遇到商品平摊价格除不尽的情况时,可以将余数加到最后一件商品的价格进行兜底

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 丸辣!BigDecimal又踩坑了
    • 前言
      • BigDecimal的坑
        • 创建实例的坑
        • toString方法的坑
        • 比较大小的坑
        • 运算的坑
        • 计算价格的坑
      • 总结
        • 最后(不要白嫖,一键三连求求拉~)
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档