一律使用 BigDecimal,避免后患?

点击上方蓝色字体,选择“设为星标”

优质文章,及时送达

52320de6fd0bab7426325ed8e0c3588f.jpeg来源:zhuanlan.zhihu.com/p/94144867

一、背景

总在项目中看到 Double 与 BigDecimal 被用错的情况,竟然有人告诉我:“一律使用 BigDecimal,避免后患”,我相信这位兄弟肯定是被精度问题搞蒙了,因此我想同步一下我的使用姿势,仅提供参考。

二、两种类型使用过程中都有可能出坑

  1. double 的计算时容易出现不精确的问题

45ece0ef1e24f47f6e366e9915f092be.png

double 的小数部分容易出现使用二进制无法准确表示。

如十进制的0.1,0.2,0.3,0.4 都不能准确表示成二进制。

具体原因相信大家都懂,我就不多说了。如果有不懂的可以私信我或者评论留言!

  1. dobule 的 = 比较要注意

ef6fdbf5d24ca603ed708e278f40224d.png

dobule 小数超过 15 位的相等比较就不一定对了。都是 true。

  1. BigDecimal 的除法除不尽会出现异常:ArithmeticException

0187df3ddda8874413296da2709f2850.jpeg

所以,使用 BigDecimal 进行除法运算一定要有精度!

  1. new BigDecimal(double) 结果也许不是你想要的

一般情况下都不使用 new BigDecimal(double) 应该使用 BigDecimal.valueOf(double)。

BigDecimal d1 = BigDecimal.valueOf(12.3);
//结果是12.3 你预期的
BigDecimal d2 = new BigDecimal(12.3);
//结果是12.300000000000000710542735760100185871124267578125

我想 12.300000000000000710542735760100185871124267578125 肯定不是你想要的结果,因此 new BigDecimal(double) 可能会产生不是你预期的结果,原理可以自行看一下底层源代码,还是比较容易搞懂的。

另:BigDecimal.valueOf(xxx) 是静态工厂类,永远优先于构造函数(摘自《Effecitve java》,此书也是非常推荐的一本经典书)

  1. BigDecimal 是不可变对象

如原来 d1=1.11 ,又加了一个数 2.11,这种操作要注意结果要指向新对象。

97b5948bee649d9ee1be9c902f892bdd.jpeg

任何针对BigDecimal对象的修改都会产生一个新对象;

BigDecimal newValue = BigDecimal.valueOf(1.2222).add(BigDecimal.valueOf(2.33333));

BigDecimal newValue = BigDecimal.valueOf(1.2222).setScale(2);

总之每次修改都要重新指向新对象,才能保证计算结果是对的。

  1. BigDecimal 比较大小操作不方便,毕竟是对象操作

比较大小和相等都使用 compareTo,如果需要返回大数或小数可使用 max,min。且注意不能使用 equals。

f88e3c29856b176a6405a739563a9543.jpeg

三、效率比较

比如:1 累加到 1000000(以本人机器 MacBookPro 2018 i7 2.2G)double 比 BigDecimal 快大约 10 倍

double: 2ms

BigDecimal:16ms

四、优缺点总结

double 的优缺点:

  1. double在计算过程中容易出现丢失精度问题

  2. 使用方便,有包装类,可自动拆装箱,计算效率高

BigDecimal 的优缺点:

  1. 精度准确,但做除法时要注意除不尽的异常

  2. BigDecimal 是对象类型,也没有自动拆封箱机制,操作起来总是有些不顺手

五、使用场景推荐

涉及到精准计算如金额,一定要使用 BigDecimal 或转成 long 或 int 计算。

若不需要精准的,如一些统计值:(本身就没有精确值)。

用户平均价格,店铺评分,用户经纬度等本就没有精准值一说的推荐使用 double 或 float,写代码更方便,计算效率也高得多。

值得一提的说,如果 double 或 float 仅是用于传值,并不会有精度问题,但如果参与了计算就要小心了。要区分是不是需要精准值,如果需要精准值,需要转成 BigDecimal 计算以后再转成 double。

但依然约定在 DTO 定义金额时使用 BigDecimal 或整形值,是为了减少或避免 double 参与金额计算的机会,避免出 bug。

其他1:代码中看到碰到让我觉得有问题的地方

以下代码在不同的类中抓出来的觉得用得不太恰当的地方:

ddfe3d0c7281124a9b5deb42b4fd1941.jpeg

代码中真的不需要那么多地方使用 BigDecimal,相反用到 BigDecimal 的地方并不多,反而用 Double 的地方更多。以上代码我希望的方式是:

fc7a9ec7482109331685dc65d9e64962.jpeg

提醒:DTO 中尽量使用包装类,防止反系列化时 null 的造成的格式转换异常

分析

经纬度:一般业务代码中也不太会去计算,仅用于传给地图api等,经纬度一般用于计算距离,如果保留到 6 位小数时其实已经是 1 米级别的了,也满足绝大多数场景了,因此使用 Double 是确实是可行的;

店铺平均消费:本身就是一个归纳统计值,也一般用来比较大小做参考,因此也用不着 BigDecimal;

当前价格:这个不一样了,为了减少 double 参与金钱计算,统一使用 BigDecimal 代替带有小数的金额;

其他2:关于 Mysql 中如何选用这两种类型

  1. 首先与 java 不同的是 mysql 是用来持久化数据的,而 java 中使用的数据一般更多的是过一下内存;

  2. 数据库都要除了指定数据类型指外还需要指定精度,因此在 DB 中 Double 计算时精度的丢失比 Java 高得多;

因为 Java 默认精确到 15-16 位了;

  1. 更改数据类型的成本,Mysql 比 Java 代码要难得多;

考虑到以上与 java 中不同几点,做点个人使用总结:

  1. 与商业金融相关字段要使用 Decimal 来表示,如金额,费率等字段;

  2. 参与各类计算如加,减,乘,除,sum,avg 等等,也要使用 Decimal;

  3. 经纬度,可以使用 double 来表示,这个可参考 Java,只要保证精度范围即可;

  4. 如果确实不确定使用什么 double 或 Decimal 哪种类型合适,那最好使用 Decimal,毕竟稳定,安全高于一切;

注:阿里的编码规范中强调统一带小数的类型一律使用 Decimal 类型,也是有道理的,使用Decimal 可以大大减少计算踩坑的概率。

2fd4ddf72f509242a6184fabbe82bbcc.gif

ed4e6888e77908dc18f27833ebc6d040.png

a83a65773fe4e960009ef8f3dc44ea0d.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值