面试官问我:你确定用了BigDecimal后,计算结果一定精确?
过年了,年终也领完了,这不打算出去面试一波,看看自己在市场中的价值,于是我简单的做了波简历,然后去面试一波,结果谁知,第一个面试就差点碰壁了,面试官竟然问我BigDecimal这个类,可是我不慌,心中有料,内心不慌,于是轻松拿下了一波高薪offer
BigDecimal,这个类其实对于经常接触金融、电商、支付的猿猿来说不算陌生,我也还算是熟悉,我也经常用,但是很多时候我们只知道他的用法,并不知道他还有隐藏的细节
首先,这是java.math包中提供的一种可以用来进行更高精度运算的类型,相较于double、float这些类型来说,BigDecimal在和金额计算打交道应该说有着天然的优势,这个大家也很熟悉了,接下来我们一起来分析下BigDecimal中的哪些注意事项
1、BigDecimal不能使用equals方法做等值比较
2、BigDecimal使用double初始化时存在精度风险
这个问题其实真的是很细节了,不知道大家有没有注意到,在《阿里巴巴Java开发手册》中其实也有注明
不知道你们在比较BigDecimal的时候都是怎么使用的,但是千万不要用==这种方式来使用哦,这个应该不用多说吧,BigDecimal属于对象,不是基本类型,不能用==来比较
一般说到这里,大家就知道了,对象的话肯定使用equals来进行比较咯,这样就没问题了,告诉你,用equals比较也有问题
你个渣,我怀疑你在骗我,那你告诉我为何,还有怎么解决?
那我该如何比较呢,自定义个类,继承BigDecimal,重写equals,当然可以。但是其实有更好的办法,在BigDecimal内部提供了compareTo方法买这个方法可以直接判断两个数字的值,相等则返回0
知其然,也要知其所以然,我肯定会解释清楚的嘞
我们来看个例子:
BigDecimal bigDecimal1 = new BigDecimal(1);
BigDecimal bigDecimal2 = new BigDecimal(1);
System.out.println(bigDecimal1.equals(bigDecimal2));
BigDecimal bigDecimal3 = new BigDecimal(1);
BigDecimal bigDecimal4 = new BigDecimal(1.0);
System.out.println(bigDecimal3.equals(bigDecimal4));
BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");
System.out.println(bigDecimal5.equals(bigDecimal6));
以上代码输出结果:true true false
有的时候结果是true,有的时候结果却是false,很奇怪,为什么呢?我们来看下BigDecimal的equals的源码:
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
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());
}
里面有一个scale标度的比较,大概这就是为什么bigDecimal5和bigDeclmal6的比较结果是false的原因了。equals不仅会比较数值,还会比较这个标度是否一样
使用equals进行比较的时候会比较数值大小和scale标度问题,那为什么上面的bigDecimal1和2、bigDecimal3和4却是相同的呢,难道是因为他们的类型是int、long,而bigDecimal5和6的类型是string,导致出现精度问题?
BigDecimal有四种定义的类型,包括int、long、double、String四种,首先int和long类型都是整数,标度都是0。当类型是double的时候,new Bigdecimal(double) => new BigDecimale(0.1),实际传入的是0.1000000000000000055511151231527827021181583404541015625,这个时候的标度就是55,也就是小数点的个数。
而对于 new bigDecimal(1.0)来说,实际上就是整数,也就是不存在后缀,所以和整数的标度大小是一样的
对于BigDecimal(String)来说,当我们传入一个字符串的时候,new BigDecimal("0.1")创建一个BigDecimal的时候,其实创建出来的值正好就是等于0.1的,那么他的标题也就是1。如果使用的是new BigDecimal("0.10000"),此时标度就是5,所以这也就是解释了为什么最后的bigDecimal5和6的结果不一样咯
那如何解决呢?其实BigDecimal不仅提供了equals方法,还提供了一个compareTo()方法,这个方法其实就是只比较两个数值的大小,感兴趣的可以去研究研究
BigDecimal使用double初始化时存在精度风险,那这是怎么一回事呢?其实在阿里开发手册中也有这么一条建议,或者说是要求吧
禁止使用构造方法BigDecimal(double)的方式把double值转化成BigDecimal对象
我们知道,计算机是只认识二进制的,只认识0和1,也就是说任何数据都会转化成0和1存储在计算机中,整数简单,除二取余,逆序排列即可。而小数则不一定全部能转化成二进制,比如0.1,在转换的过程中会出现循环的情况,所以这种事无法正确的存储完整的数据的,计算机是无法精确的存储这种数据的,所以计算机采用的是一定的精度来解决这个问题的,这就是IEEE 754(IEEE二进制浮点数算术标准)规范的主要思想。
IEEE 754规定了多种表示浮点数值的方式,其中最常用的就是32位单精度浮点数和64位双精度浮点数。
在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。
所谓精度不同,可以简单的理解为保留有效位数不同。采用保留有效位数的方式近似的表示小数。
如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。
在BigDecimal中,标度是通过scale字段来表示的。
而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。
涉及到的字段就是这几个:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
private final transient long intCompact;
}
大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。
那么标度到底是什么呢?除了scale这个字段,在BigDecimal中还提供了scale()方法,用来返回这个BigDecimal的标度。那么,scale到底表示的是什么,其实上面的注释已经说的很清楚了:
如果scale为零或正值,则该值表示这个数字小数点右侧的位数。如果scale为负数,则该数字的真实值需要乘以10的该负数的绝对值的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。
如123.123,那么如果使用BigDecimal表示,那么他的无标度值为123123,他的标度为3。
而二进制无法表示的0.1,使用BigDecimal就可以表示了,及通过无标度值1和标度1来表示。
我们都知道,想要创建一个对象,需要使用该类的构造方法,在BigDecimal中一共有以下4个构造方法:
其中 BigDecimal(int)和BigDecimal(long) 比较简单,因为都是整数,所以他们的标度都是0。而BigDecimal(double) 和BigDecimal(String)的标度就有很多学问了。
BigDecimal中虽然提供了一个通过double创建BigDecimal的方法,但是这其中也挖下了一个坑
我们知道,double表示的小数是不精确的,比如0.1这个数值,double只能表示他的近似值,所以当我们使用new BigDecimal(0.1)的时候,实际上创建出来的数值并不是正好等于0.1的,而是一个近似值
所以,如果我们在代码中,使用BigDecimal(double) 来创建一个BigDecimal的话,那么是损失了精度的,这是极其严重的。
那么,该如何创建一个精确的BigDecimal来表示小数呢,答案是使用String创建。
而对于BigDecimal(String) ,当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。
那么他的标度也就是1。
但是需要注意的是,new BigDecimal("0.10000")和new BigDecimal("0.1")这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false,可以使用compareTo方法进行比较
那么,想要创建一个能精确的表示0.1的BigDecimal,请使用以下两种方式:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
好了,以上就是全部内容了,我是小仙人,你们的学习成长小伙伴
我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。
再次感谢大家能够读到这里,我后面会持续的更新技术文章以及一些记录生活的灵魂文章,如果觉得不错的,觉得【小仙】有点东西的话,求点赞、关注、分享三连
哦,对了!后续的更新文章我都会及时放到这里,欢迎大家点击观看,都是干货文章啊,建议收藏,以后随时翻阅查看
https://github.com/DayuMM2021/Java