当前位置: 首页 > 科技观察

不掌握这些坑你还敢用BigDecimal吗?_0

时间:2023-03-12 04:07:19 科技观察

背??景一直从事金融相关的项目,所以对BigDecimal非常熟悉,也见过很多同学不知道、不理解、或者使用不当,导致资金流失的事件。所以,如果你从事的是金融相关的项目,或者你的项目涉及到金额的计算,那么一定要花时间阅读这篇文章,全面了解BigDecimal。BigDecimal概述Java在java.math包中提供了一个API类BigDecimal,用于对有效位数大于16位的数字进行精确运算。双精度浮点变量double可以处理16位有效数字,但在实际应用中,可能需要对更大或更小的数进行运算处理。一般情况下,对于不需要精确计算精度的数字,可以直接使用Float和Double,但是Double.valueOf(String)和Float.valueOf(String)会损失精度。所以如果需要准确的计算结果,就必须使用BigDecimal类来操作。BigDecimal对象提供了+、-、*、/等传统算术运算符对应的方法,通过这些方法进行相应的运算。BigDecimal是不可变的,每进行一次四次运算都会产生一个新的对象,所以做加减乘除时记得保存运算后的值。BigDecimal的4大坑在使用BigDecimal的时候,在使用场景中有4种坑。你必须明白,如果你使用不当,那一定很惨。掌握了这些案例,当别人写出有坑的代码时,你一眼就能认出来。大牛就是这么修炼的。第一:浮点类型的陷阱在了解BigDecimal的陷阱之前,先说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算,得到的可能是一个近似值,而不是一个确切的价值。.例如下面的代码:@Testpublicvoidtest0(){floata=1;浮动b=0.9f;System.out.println(a-b);}结果是什么?0.1?不是,上面代码执行的结果是0.100000024。这样做的原因是0.1的二进制表示无限循环。由于计算机资源有限,没有办法用二进制准确表示0.1。只能用“近似值”来表示,即在精度有限的情况下,将接近0.1的二进制数最大化,会造成精度的损失。案件。上面的现象大家都知道,我就不详细展开了。同时会得出结论,科学计数法可以考虑浮点类型,但如果涉及到金额计算,则需要使用BigDecimal进行计算。那么,BigDecimal能不能避免上面提到的浮点数问题呢?看下面的例子:@Testpublicvoidtest1(){BigDecimala=newBigDecimal(0.01);BigDecimalb=BigDecimal.valueOf(0.01);System.out.println("a="+a);System.out.println("b="+b);上面单元测试中的代码a和b的结果是什么?a=0.01000000000000000020816681711721685132943093776702880859375b=0.01上面的例子说明即使使用BigDecimal,结果还是会有精度问题。这就涉及到创建BigDecimal对象的时候,如果有初始值,是newBigDecimal的形式还是通过BigDecimal#valueOf方法。出现上述现象的原因是在使用newBigDecimal时,传入的0.1已经是浮点型了。由于上面提到的值只是一个近似值,所以在使用newBigDecimal时,这个近似值是完整保留的。但是BigDecimal#valueOf不同,它的源代码实现如下:publicstaticBigDecimalvalueOf(doubleval){//提醒:零double返回'0.0',所以我们不能fastpath//使用常量ZERO。这可能足够重要//稍后证明工厂方法、缓存或一些私有常量的合理性。返回新的BigDecimal(Double.toString(val));}在valueOf内部,使用Double#toString方法将浮点型值转换为字符串,所以不会损失精度。至此,可以得出一个基本的结论:首先,在使用BigDecimal构造函数时,尽量传递字符串,而不是浮点类型;其次,如果第一个条件不能满足,可以使用BigDecimal#valueOf方法构造初始化值。这里延伸一下,BigDecimal的常用构造方法如下:BigDecimal(int)创建一个对象,其值为参数指定的整数值。BigDecimal(double)使用参数指定的双精度值创建一个对象。BigDecimal(long)使用参数指定的长整数值创建一个对象。BigDecimal(String)用参数指定的数值创建一个对象作为字符串。涉及到参数类型为double的构造方法,会出现上述问题,使用时要特别注意。二:浮点精度的坑如果比较两个BigDecimals的值是否相等,你会怎么比较?使用equals方法还是compareTo方法?先来看一个例子:@Testpublicvoidtest2(){BigDecimala=newBigDecimal("0.01");BigDecimalb=newBigDecimal("0.010");System.out.println(a.equals(b));System.out.println(a.compareTo(b));}乍一看似乎是相等的,但实际上它们本质上是不一样的。equals方法基于BigDecimal实现的equals方法。直观的印象就是比较两个对象是否相同,那么代码是如何实现的呢?@Overridepublicbooleanequals(Objectx){if(!(xinstanceofBigDecimal))returnfalse;BigDecimalxDec=(BigDecimal)x;如果(x==this)返回真;如果(比例!=xDec.scale)返回假;longs=this.intCompact;长xs=xDec.intCompact;if(s!=INFLATED){if(xs==INFLATED)xs=compactValFor(xDec.intVal);返回xs==s;}elseif(xs!=INFLATED)returnxs==compactValFor(this.intVal);返回this.inflated().equals(xDec.inflated());比较准确率是否相同。在上面的例子中,由于两者的精度不同,equals方法的结果当然是false。compareTo方法实现了Comparable接口,实际比较的是值的大小。返回值为-1(小于)、0(等于)和1(大于)。基本结论:一般情况下,如果比较两个BigDecimal值的大小,使用它实现的compareTo方法;如果严格限制比较精度,可以考虑使用equals方法。另外这种场景在比较0值的时候比较常见,比如比较BigDecimal("0"),BigDecimal("0.0"),BigDecimal("0.00")。这时候就必须使用compareTo方法进行比较。第三:设置精度的陷阱。在项目中看到很多同学在通过BigDecimal计算时,没有设置计算??结果的精度和舍入方式。我真的很着急,虽然大多数情况下不会有问题。但是下面的场景就不一定了:@Testpublicvoidtest3(){BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("3.0");a.划分(b);}执行上面的代码,结果是什么?ArithmeticException异常!java.lang.ArithmeticException:非终止小数展开;没有可精确表示的十进制结果。atjava.math.BigDecimal.divide(BigDecimal.java:1690)...这个异常的发生在官方文档中也有说明:如果商有一个不终止的小数展开并且指定操作返回一个精确的结果,抛出ArithmeticException。否则,与其他操作一样,返回除法的确切结果。综上所述,如果在除法(divide)运算过程中,如果商是无限小数(0.333...),而期望运算的结果是一个精确的数,则会抛出ArithmeticException。此时只要指定使用divide方法时结果的精度即可:@Testpublicvoidtest3(){BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("3.0");BigDecimalc=a.divide(b,2,RoundingMode.HALF_UP);System.out.println(c);}执行以上代码,输入结果为0.33。基本结论:在使用BigDecimal进行(所有)操作时,一定要明确指定精度和舍入模式。展开一下,在RoundingMode枚举类中定义了舍入模式,一共有8种:RoundingMode.UP:从零开始舍入的舍入模式。在丢弃非零部分之前始终增加数字(始终将1添加到非零丢弃部分之前的数字)。请注意,这种舍入模式永远不会减少计算值的大小。RoundingMode.DOWN:接近零的舍入模式。在丢弃部分之前,切勿增加数字(切勿将1添加到丢弃部分之前的数字,即截断)。请注意,这种舍入模式永远不会增加计算值的大小。RoundingMode.CEILING:接近正无穷大的舍入模式。如果BigDecimal为正,则舍入行为与ROUNDUP相同;如果为负,则舍入行为与ROUNDDOWN相同。请注意,这种舍入模式永远不会减少计算值。RoundingMode.FLOOR:接近负无穷大的舍入模式。如果BigDecimal为正,则舍入行为与ROUNDDOWN相同;如果为负,则舍入行为与ROUNDUP相同。请注意,这种舍入模式永远不会增加计算值。RoundingMode.HALF_UP:四舍五入到“最接近”的数,如果相邻两个数的距离相等,则为向上取整的舍入模式。如果丢弃的分数>=0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。注意,这是我们小学时学过的舍入模式(roundingup)。RoundingMode.HALF_DOWN:四舍五入到“最近”的数,如果相邻两个数的距离相等,则为四舍五入的一种舍入方式。如果丢弃的分数>0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN(向上舍入)相同。RoundingMode.HALF_EVEN:向“最近的”数字舍入,或者如果到两个相邻数字的距离相等,则向相邻的偶数舍入。如果丢弃部分左边的数字是奇数,则舍入行为与ROUNDHALFUP相同;如果是偶数,则舍入行为与ROUNDHALF_DOWN相同。请注意,这种舍入模式可最大限度地减少重复一系列计算时的累积误差。这种舍入模式也称为“银行家舍入”,主要在美国使用。四舍五入,在两种情况下获得五分。如果前一位是奇数,则放入,否则舍弃。下面的例子保留了1位小数,所以这个四舍五入方法的结果。1.15==>1.2,1.25==>1.2RoundingMode.UNNECESSARY:断言请求的操作有一个精确的结果,因此不需要舍入。如果为获得精确结果的操作指定了此舍入模式,则会抛出ArithmeticException。通常我们使用的舍入是RoundingMode.HALF_UP。第四:三种字符串输出的坑先看下面这段代码:@Testpublicvoidtest4(){BigDecimala=BigDecimal.valueOf(35634535255456719.22345634534124578902);System.out.println(a.toString());}执行的结果是上面对应的值吗?不是:3.563453525545672E+16也就是说,我本来想打印一个字符串,但是结果是科学计数的值。这里需要了解一下BigDecimal将字符串转换为PlainString()的三个方法:不要使用任何科学计数法;toString():必要时使用科学记数法;toEngineeringString():必要时使用工程符号。类似于科学记数法,只是指数的次方是3的倍数,方便工程应用,因为转换很多单位时是10^3;三种方法的结果示例如下:计算方法的基本结论:**根据数据结果的不同显示格式,采用不同的字符串输出方式,通常使用的方法是toPlainString()**。另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,BigDecimal可以用来控制货币值、百分比值以及超过16位有效数字的一般数值的格式化。使用示例如下:NumberFormatcurrency=NumberFormat.getCurrencyInstance();//创建货币格式引用NumberFormatpercent=NumberFormat.getPercentInstance();//创建百分比格式参考percent.setMaximumFractionDigits(3);//百分比小数点最多3位BigDecimalloanAmount=newBigDecimal("15000.48");//金额BigDecimalinterestRate=newBigDecimal("0.008");//利率BigDecimalinterest=loanAmount.multiply(interestRate);//相乘println("利息:\t"+currency.format(利息));输出结果如下:金额:¥15,000.48利率:0.8%利息:¥120.00总结本文介绍了BigDecimal使用场景的陷阱以及基于这些陷阱的“最佳实践”。虽然在某些场景下推荐使用BigDecimal,可以获得更好的精度,但是相对于double和float,在性能上还是有一定的损失,尤其是在处理大而复杂的运算时。因此,一般的精度计算没有必要使用BigDecimal。必须使用的时候,一定要避开上面提到的那些坑。