Java-BigDecimal踩坑记录
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.结论
能不用除法就不用除法