掌握BigDecimal:详解其原理及最佳实践


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


BigDecimalJava中用于浮点数数值计算的类,其主要适合用于处理需要精确表示和运算的场景。BigDecimal不仅能精确表示非常大的或非常小的数字,同时还提供任意精度的运算。其有效的解决了浮点数(floatdouble)在进行精确计算时可能出现的舍入误差问题。

本文主要介绍了BigDecimal数据存储的原理以及开发中BigDecimal使用的最佳实践。以加深读者对于BigDecimal的理解。

BigDecimal简介

在处理金融、科学等领域的计算时,为了解决doublefloat在计算值存在的精度缺失问题 BigDecimal应运而生。BigDecimal在设计之初皆在提供更高的精度和准确性,以确保浮点数运算的准确性。因此其具有如下特点:

  1. 高精度BigDecimal能够精确表示非常大的或非常小的数字,并且提供任意精度的运算。
  2. 不可变性BigDecimal对象是不可变的。一旦创建,数值就不会改变。所有的算术运算都会返回一个新的BigDecimal对象,而不会修改原来的对象。这种设计使得BigDecimal是线程安全的。
  3. 丰富的运算方法BigDecimal提供了丰富的算术运算方法,如add(加法)、subtract(减法)、multiply(乘法)和divide(除法),以及用于舍入、取整和比较的方法。
  4. 灵活的舍入模式:提供多种舍入模式(如四舍五入、向上取整等),确保结果的精度和舍入行为可控。

总的来看,BigDecimal通过其对象的不可变性,从而确保了线程安全;与此同时,其还并提供丰富的算术运算方法(如加法、减法、乘法、除法)和多种舍入模式(如四舍五入、向上取整等),从而满足精确数值计算的需求。

BigDecimal数据存储的秘密

BigDecimal有了基础认识后,接下来我们便通过Debug的形式来看看BigDecimal内部究竟是如来实现数据的高精度的存储的。为此我们首先通过如下的语句来构建一个BigDecimal对象

java 复制代码
BigDecimal bigDecimal = new BigDecimal("3.1415926");

运行代码进入IdeaDebug模式后,可以看到如下内容:

不难发现,对于BigDecimal对象而言其内部有 intVal、scal、precision、stringCache、initCompact等五个重要属性。 进一步,翻开BigDecimal源码,可以看到这五个属性各自对应的类型:

java 复制代码
public class BigDecimal extends Number implements Comparable<BigDecimal> {
    
    private final BigInteger intVal;

    private final int scale;  
                              
    private transient int precision;

    private transient String stringCache;

  
    private final transient long intCompact;

具体来看,intVal为一个BigInteger对象,其主要用于保存超出基本类型的数值。 例如:对于Long数据类型来看,其最大类型为0x7fffffffffffffff9223372036854775807。因此如下的赋值BigDecimal bigDecimal = new BigDecimal("9223372036854775808")其已然超出了Java中基础类型所能表示的范围,而此时在bigDecimal对象中,其内部的intVal如下所示,不难发现9223372036854775808被赋值给intVal

明白了BigDecimalintVal属性的存储规则后,再来看其中的scale、precision所标示的含义。其中scale表示小数点后的位数而precision则代表 BigDecimal中数据的总位数,即包括整数和小数部分。

进一步,BigDecimalstringCache属性则主要用于保存BigDecimal数据所转成的字符串信息,而intCompact则用于将long数值以内的数据转为基本数据类型long进行存储。

(注:如果数据类型范围超过long所能表示的范围,则会将数据保存至intVal中)

此外还要注意一点,如果是包含小数点的数据其会将其小数点去掉,进而保存其去掉小数点后的数据。例如new BigDecimal("3.1415926")在该BigDecimal对象中intCompact = 31415926

BigDecimal的最佳实践

知晓了BigDecimal内部对于浮点数据的存储原理后,接下来我们来谈一谈有关BigDecimal的几点最佳实践,以避免在使用BigDecimal时踩坑。

  1. 为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val)

如果使用double 类型的数据来构建一个 BigDecimal 对象时,其会出现精度丢失的问题。这主要是因为 double 类型本身在表示浮点数时存在精度限制。

具体来看,double 类型使用 IEEE 754 标准的双精度浮点数格式,该格式在二进制表示中无法精确地表示所有十进制的小数。例如,十进制数 0.1 在二进制浮点数中是一个无限循环小数,只能近似表示为 0.1000000000000000055511151231257827021181583404541015625。而使用 new BigDecimal(double) 构造函数时double类型的数值的会将其近似值传递给 BigDecimal,进入导致精度丢失。例如:

java 复制代码
    
     double value = 0.1;
     BigDecimal bd = new BigDecimal(value);
     System.out.println(bd); 

上述代码最终会输出:0.1000000000000000055511151231257827021181583404541015625而我们所期待的 BigDecimal 实际为 0.1。因此为了避免构建BigDecimal时出现精度丢失的问题,推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建对象。

  1. 使用 BigDecimal进行除法运算时,指明数据结果的精度

BigDecimal 在进行除法运算时,如果不指定截取的精度和舍入模式,当出现数据无法整除时,会出现 ArithmeticException 异常 。例如 1 / 3时其会得到一个无限循环小数。这时如果没有明确指定精度和舍入方式,BigDecimal 将无法完成除法运算并抛出异常。

java 复制代码
public class BigDecimalDivisionExample {
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("1");
        BigDecimal num2 = new BigDecimal("3");
        BigDecimal result = 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,则舍入到最接近的偶数。
  1. 根据业务需要,合理的使用compareToequals

由于BigDecimal 内部对 equals方法逻辑进行了重写,这使得equals方法不仅比较数值部分,还比较标度。因此只有数值和标度都相同时equals 方法才会返回 true。例如:

java 复制代码
public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal bd1 = new BigDecimal("1.0");
        BigDecimal bd2 = new BigDecimal("1.00");
        
        System.out.println(bd1.equals(bd2)); // 输出 false
    }
}

在这个例子中,bd1 = 1.0 bd2 = 1.00 两个数的数值部分代表的含义是完全相同的,但其精度却不同,此时如果使用equals 方法进行比较,则会返回 false。如果贸然使用equals 是很容易导致出现意料之外的结果。

为了保证数值的比较,BigDecimal 内部也对compareTo 方法进行了重写,使得compareTo方法只比较BigDecimal的数值部分而不考虑标度。因此如果两个 BigDecimal对象的数值相等,即使标度不同compareTo 方法也会认为它们相等。

java 复制代码
public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal bd1 = new BigDecimal("1.0");
        BigDecimal bd2 = new BigDecimal("1.00");

        System.out.println(bd1.compareTo(bd2)); // 
    }
}

在这个例子中最终的输出结果为0,即代表bd1bd2相等。这主要是因为bd1bd2的数值相等因此compareTo 方法返回 0

因此对于为了避免不必要的混淆和错误,尽量遵循以下最佳实践:

  • 明确比较目的 :在使用 BigDecimal 进行比较时,首先明确你的比较目的是检查数值相等还是完全相等(包括标度)。如果仅比较数值,请使用 compareTo 方法。如果需要完全相等,请使用 equals 方法。
  • 避免误解 :理解 BigDecimalequals 方法会考虑标度,而 compareTo 方法只比较数值。在常见的数值比较中,更推荐使用 compareTo 方法。
  1. 慎用BigDecimaltoString方法

BigDecimal内部对toString方法进行重载,这使得BigDecimaltoString 方法会自动去除尾随零,并且使用科学计数法表示非常大的或非常小的数值。例如:

java 复制代码
public class BigDecimalToStringExample {
    public static void main(String[] args) {
        BigDecimal bd1 = new BigDecimal("123.4500");
        BigDecimal bd2 = new BigDecimal("0.00012345");

        System.out.println(bd1.toString()); 
        System.out.println(bd2.toString()); 
    }
}

上述代码分别会输出123.45、1.2345E-4。其中bd1 的尾随零被去除,而 bd2 使用了科学计数法进行数据的表示。而为了避免这类问题的发生,可以使用 BigDecimaltoPlainString 方法。该方法不会去除尾随零,也不会使用科学计数法。

java 复制代码
import java.math.BigDecimal;

public class BigDecimalToPlainStringExample {
    public static void main(String[] args) {
        BigDecimal bd1 = new BigDecimal("123.4500");
        BigDecimal bd2 = new BigDecimal("0.00012345");
        

        System.out.println(bd1.toPlainString()); // 输出 123.4500
        System.out.println(bd2.toPlainString()); // 输出 0.00012345

    }
}

不难看出,在这个例子中toPlainString 方法保留了尾随零,并且没有使用科学计数法,输出格式更加直观。

  • toString 方法:自动去除尾随零,使用科学计数法表示非常大或非常小的数值。可能导致格式不符合预期。
  • toPlainString 方法:不会去除尾随零,不会使用科学计数法,适合需要保留原始数值格式的场景。

总结

本文主要对BigDecimal内部对于浮点数的存储规则进行分析,以加深读者对于BigDecimal的理解。同时整理了如下五条BigDecimal使用的最佳实践:

  1. 为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val)
  2. 使用 BigDecimal进行除法运算时,指明数据结果的精度;
  3. 根据业务需要,合理的使用compareToequals
  4. 慎用BigDecimaltoString方法。
相关推荐
向阳12185 分钟前
Dubbo负载均衡
java·运维·负载均衡·dubbo
Gu Gu Study15 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
测试199824 分钟前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
WaaTong38 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_7430484438 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries40 分钟前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
小灰灰__1 小时前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
马剑威(威哥爱编程)1 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理