掌握BigDecimal:详解其原理及最佳实践
点击关注公众号,“技术干货”及时达!?思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航??
?BigDecimal是Java中用于浮点数数值计算的类,其主要适合用于处理需要精确表示和运算的场景。「BigDecimal不仅能精确表示非常大的或非常小的数字,同时还提供任意精度的运算。其有效的解决了浮点数(float和double)在进行精确计算时可能出现的舍入误差问题。」
本文主要介绍了BigDecimal数据存储的原理以及开发中BigDecimal使用的最佳实践。以加深读者对于BigDecimal的理解。
BigDecimal简介在处理金融、科学等领域的计算时,为了解决double或float在计算值存在的精度缺失问题BigDecimal应运而生。BigDecimal在设计之初「皆在提供更高的精度和准确性,以确保浮点数运算的准确性」。因此其具有如下特点:
「高精度」:BigDecimal能够精确表示非常大的或非常小的数字,并且提供任意精度的运算。「不可变性」:BigDecimal对象是不可变的。一旦创建,数值就不会改变。所有的算术运算都会返回一个新的BigDecimal对象,而不会修改原来的对象。这种设计使得BigDecimal是线程安全的。「丰富的运算方法」:BigDecimal提供了丰富的算术运算方法,如add(加法)、subtract(减法)、multiply(乘法)和divide(除法),以及用于舍入、取整和比较的方法。「灵活的舍入模式」:提供多种舍入模式(如四舍五入、向上取整等),确保结果的精度和舍入行为可控。总的来看,「BigDecimal通过其对象的不可变性,从而确保了线程安全;与此同时,其还并提供丰富的算术运算方法(如加法、减法、乘法、除法)和多种舍入模式(如四舍五入、向上取整等),从而满足精确数值计算的需求。」
BigDecimal数据存储的秘密对BigDecimal有了基础认识后,接下来我们便通过Debug的形式来看看BigDecimal内部究竟是如来实现数据的高精度的存储的。为此我们首先通过如下的语句来构建一个BigDecimal对象
BigDecimalbigDecimal=newBigDecimal("3.1415926");
运行代码进入Idea的Debug模式后,可以看到如下内容:
不难发现,对于BigDecimal对象而言其内部有 「intVal、scal、precision、stringCache、initCompact等五个重要属性。」 进一步,翻开BigDecimal源码,可以看到这五个属性各自对应的类型:
publicclassBigDecimalextendsNumberimplementsComparableBigDecimal{
privatefinalBigIntegerintVal;
privatefinalintscale;
privatetransientintprecision;
privatetransientStringstringCache;
privatefinaltransientlongintCompact;
具体来看,「intVal为一个BigInteger对象,其主要用于保存超出基本类型的数值。」 例如:对于Long数据类型来看,其最大类型为0x7fffffffffffffff即9223372036854775807。因此如下的赋值BigDecimal bigDecimal = new BigDecimal("9223372036854775808")其已然超出了Java中基础类型所能表示的范围,而此时在bigDecimal对象中,其内部的intVal如下所示,不难发现9223372036854775808被赋值给intVal。
image.png明白了BigDecimal中intVal属性的存储规则后,再来看其中的scale、precision所标示的含义。「其中scale表示小数点后的位数而precision则代表BigDecimal中数据的总位数,即包括整数和小数部分。」
进一步,「BigDecimal的stringCache属性则主要用于保存BigDecimal数据所转成的字符串信息,而intCompact则用于将long数值以内的数据转为基本数据类型long进行存储。」
(注:如果数据类型范围超过long所能表示的范围,则会将数据保存至intVal中)
此外还要注意一点,如果是包含小数点的数据其会将其小数点去掉,进而保存其去掉小数点后的数据。例如new BigDecimal("3.1415926")在该BigDecimal对象中intCompact = 31415926。
BigDecimal的最佳实践知晓了BigDecimal内部对于浮点数据的存储原理后,接下来我们来谈一谈有关BigDecimal的几点最佳实践,以避免在使用BigDecimal时踩坑。
?为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者BigDecimal.valueOf(double val)?如果使用double 类型的数据来构建一个 BigDecimal 对象时,其会出现精度丢失的问题。这主要是因为 double 类型本身在表示浮点数时存在精度限制。
具体来看,double 类型使用 IEEE 754标准的双精度浮点数格式,该格式在二进制表示中无法精确地表示所有十进制的小数。例如,十进制数 0.1 在二进制浮点数中是一个无限循环小数,只能近似表示为 0.1000000000000000055511151231257827021181583404541015625。而使用 new BigDecimal(double) 构造函数时double类型的数值的会将其近似值传递给 BigDecimal,进入导致精度丢失。例如:
doublevalue=0.1;
BigDecimalbd=newBigDecimal(value);
System.out.println(bd);
上述代码最终会输出:0.1000000000000000055511151231257827021181583404541015625而我们所期待的 BigDecimal 实际为 0.1。因此为了避免构建BigDecimal时出现精度丢失的问题,「推荐使用它的BigDecimal(String val)构造方法或者BigDecimal.valueOf(double val)静态方法来创建对象。」
?使用 BigDecimal进行除法运算时,指明数据结果的精度?BigDecimal 在进行除法运算时,「如果不指定截取的精度和舍入模式,当出现数据无法整除时,会出现 ArithmeticException 异常」。例如 1 / 3时其会得到一个无限循环小数。这时如果没有明确指定精度和舍入方式,BigDecimal 将无法完成除法运算并抛出异常。
publicclassBigDecimalDivisionExample{
publicstaticvoidmain(String[]args){
BigDecimalnum1=newBigDecimal("1");
BigDecimalnum2=newBigDecimal("3");
BigDecimalresult=num1.divide(num2);
}
在上述代码中,num1 / num2的结果为一个无限循环小数 0.333...。「由于我们并未在代码中指定精度和舍入模式,所以当执行上述代码时如出现如下异常」:Exception: Non-terminating decimal expansion; no exact representable decimal result.
为了避免上述异常的发生,可以再执行divide显示的指定精度截取方式。具体方式如下:num1.divide(num2,2, RoundingMode.HALF_UP);在本例中对数据保留了两位小数,同时使用RoundingMode.HALF_UP四舍五入的截取方式。
事实上 BigDecimal除了外RoundingMode.HALF_UP的舍入方式外,还有如下的截取方式:
RoundingMode.HALF_UP:四舍五入,向上舍入。RoundingMode.HALF_DOWN:四舍五入,向下舍入。RoundingMode.HALF_EVEN:四舍五入,如果舍弃部分等于0.5,则舍入到最接近的偶数。?根据业务需要,合理的使用compareTo和equals?由于BigDecimal 内部对 equals方法逻辑进行了重写,这使得equals方法不仅比较数值部分,还比较标度。因此只有数值和标度都相同时equals 方法才会返回 true。例如:
publicclassBigDecimalComparison{
publicstaticvoidmain(String[]args){
BigDecimalbd1=newBigDecimal("1.0");
BigDecimalbd2=newBigDecimal("1.00");
System.out.println(bd1.equals(bd2));//输出false
}
}
在这个例子中,bd1 = 1.0 bd2 = 1.00 两个数的数值部分代表的含义是完全相同的,但其精度却不同,此时如果使用equals 方法进行比较,则会返回 false。如果贸然使用equals 是很容易导致出现意料之外的结果。
为了保证数值的比较,BigDecimal 内部也对compareTo 方法进行了重写,使得compareTo方法只比较BigDecimal的数值部分而不考虑标度。「因此如果两个 BigDecimal对象的数值相等,即使标度不同compareTo 方法也会认为它们相等。」
publicclassBigDecimalComparison{
publicstaticvoidmain(String[]args){
BigDecimalbd1=newBigDecimal("1.0");
BigDecimalbd2=newBigDecimal("1.00");
System.out.println(bd1.compareTo(bd2));//
}
}
在这个例子中最终的输出结果为0,即代表bd1和bd2相等。这主要是因为bd1和bd2的数值相等因此compareTo 方法返回 0。
因此对于为了避免不必要的混淆和错误,尽量遵循以下最佳实践:
「明确比较目的」:在使用 BigDecimal 进行比较时,首先明确你的比较目的是检查数值相等还是完全相等(包括标度)。如果仅比较数值,请使用 compareTo 方法。如果需要完全相等,请使用 equals 方法。「避免误解」:理解 BigDecimal 的 equals 方法会考虑标度,而 compareTo 方法只比较数值。在常见的数值比较中,更推荐使用 compareTo 方法。?慎用BigDecimal的toString方法?BigDecimal内部对toString方法进行重载,这使得BigDecimal 的 toString 方法会自动去除尾随零,并且使用科学计数法表示非常大的或非常小的数值。例如:
publicclassBigDecimalToStringExample{
publicstaticvoidmain(String[]args){
BigDecimalbd1=newBigDecimal("123.4500");
BigDecimalbd2=newBigDecimal("0.00012345");
System.out.println(bd1.toString());
System.out.println(bd2.toString());
}
}
上述代码分别会输出123.45、1.2345E-4。其中bd1 的尾随零被去除,而 bd2 使用了科学计数法进行数据的表示。而为了避免这类问题的发生,可以使用 BigDecimal 的 toPlainString 方法。该方法不会去除尾随零,也不会使用科学计数法。
importjava.math.BigDecimal;
publicclassBigDecimalToPlainStringExample{
publicstaticvoidmain(String[]args){
BigDecimalbd1=newBigDecimal("123.4500");
BigDecimalbd2=newBigDecimal("0.00012345");
System.out.println(bd1.toPlainString());//输出123.4500
System.out.println(bd2.toPlainString());//输出0.00012345
}
}
不难看出,在这个例子中toPlainString 方法保留了尾随零,并且没有使用科学计数法,输出格式更加直观。
「toString 方法」:自动去除尾随零,使用科学计数法表示非常大或非常小的数值。可能导致格式不符合预期。「toPlainString 方法」:不会去除尾随零,不会使用科学计数法,适合需要保留原始数值格式的场景。总结本文主要对BigDecimal内部对于浮点数的存储规则进行分析,以加深读者对于BigDecimal的理解。同时整理了如下五条BigDecimal使用的最佳实践:
为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者BigDecimal.valueOf(double val);使用 BigDecimal进行除法运算时,指明数据结果的精度;根据业务需要,合理的使用compareTo和equals;慎用BigDecimal的toString方法。
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线