丸辣!BigDecimal又踩坑了丸辣!

丸辣!BigDecimal又踩坑了

前言

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

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

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

...

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

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

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

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

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

scale字段:存储小数的位数

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

BigDecimal的坑

创建实例的坑

错误示例:

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

整理了这份Java面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

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

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

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值