• BigDecimal 使用注意!!“别踩坑”


    目录

    前言

    BigDecimal概述

    BigDecimal常用构造函数

    BigDecimal常用方法详解

    相除时的舍入方法不同精度表示

    BigDecimal大小比较

    常用使用时需注意如下整理的避坑点


    前言

            在互联网金融业务领域对于金额的使用、计算等,Java常用BigDecimal类型对数据精度、精确计算等要求场景内。但在使用该java.math.BigDecimal类时,注意不要踩坑。用错可能会有大问题,大灾难!金额无小事!资损无小事!本文用于介绍BigDecimal的常用方法及整理的小小的扫盲避坑指南。

    BigDecimal概述

            Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。

    BigDecimal常用构造函数

    1. BigDecimal(int)

      创建一个具有参数所指定整数值的对象

    2. BigDecimal(double)

      创建一个具有参数所指定双精度值的对象

    3. BigDecimal(long)

      创建一个具有参数所指定长整数值的对象

    4. BigDecimal(String)

      创建一个具有参数所指定以字符串表示的数值的对象

            BigDecimal的使用的第一步就是创建一个BigDecimal对象,如果这一步都有问题,那么后面怎么算都是错的!

    那到底应该如何正确的创建一个BigDecimal?

    关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求:

            这是一条【强制】建议,那么,这背后的原理是什么呢?想要搞清楚这个问题,主要需要弄清楚以下几个问题:

    1、为什么说double不精确?

    2、BigDecimal是如何保证精确的?

            在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。double为什么不精确?首先,计算机是只认识二进制的,即0和1,这个大家一定都知道。那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。

    但是,小数的二进制如何表示呢?

            十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数

            所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。

    在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。所谓精度不同,可以简单的理解为保留有效位数不同。采用保留有效位数的方式近似的表示小数。

    所以,大家也就知道为什么double表示的小数不精确了。

    BigDecimal如何精确计数?

            如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。在BigDecimal中,标度是通过scale字段来表示的。

            而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。

    BigDecimal常用方法详解

            BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。

    • add(BigDecimal) BigDecimal对象中的值相加,然后返回这个对象。
    • subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象。
    • multiply(BigDecimal) BigDecimal对象中的值相乘,然后返回这个对象。
    • divide(BigDecimal) BigDecimal对象中的值相除,然后返回这个对象。
    • toString() 将BigDecimal对象的数值转换成字符串。
    • doubleValue() 将BigDecimal对象中的值以双精度数返回。
    • floatValue() 将BigDecimal对象中的值以单精度数返回。
    • longValue() 将BigDecimal对象中的值以长整数返回。
    • intValue() 将BigDecimal对象中的值以整数返回。

    相除时的舍入方法不同精度表示

    BigDecimal舍入模式

            尽管数据库存储的是一个高精度的浮点数,但是通常在应用中展示的时候往往需要限制一下小数点的位数,比如两到三位小数即可,这时就需要使用到setScale(int newScale, int roundingMode)函数,作为BigDecimal的公有静态变量,舍入模式(Rounding Mode)的运算规则比较多,共有八种,这里作个说明,官方文档也有介绍。

    • ROUND_UP
      向远离零的方向舍入。舍弃非零部分,并将非零舍弃部分相邻的一位数字加一。
    • ROUND_DOWN
      向接近零的方向舍入。舍弃非零部分,同时不会非零舍弃部分相邻的一位数字加一,采取截取行为。
    • ROUND_CEILING
      向正无穷的方向舍入。如果为正数,舍入结果同ROUND_UP一致;如果为负数,舍入结果同ROUND_DOWN一致。注意:此模式不会减少数值大小。
    • ROUND_FLOOR
      向负无穷的方向舍入。如果为正数,舍入结果同ROUND_DOWN一致;如果为负数,舍入结果同ROUND_UP一致。注意:此模式不会增加数值大小。
    • ROUND_HALF_UP
      向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分>= 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。这种模式也就是我们常说的我们的“四舍五入”。
    • ROUND_HALF_DOWN
      向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向下舍入的舍入模式。如果舍弃部分> 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。这种模式也就是我们常说的我们的“五舍六入”。
    • ROUND_HALF_EVEN
      向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则相邻的偶数舍入。如果舍弃部分左边的数字奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。注意:在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况,如果前一位为奇数,则入位,否则舍去。
    • ROUND_UNNECESSARY
      断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

    BigDecimal大小比较

    java中对BigDecimal比较大小一般用的是BigDecimal的compareTo方法。

    举例如:

    int a = bigdemical.compareTo(bigdemical2)

    返回结果分析:

    a = -1,表示bigdemical小于bigdemical2;

    a = 0,表示bigdemical等于bigdemical2;

    a = 1,表示bigdemical大于bigdemical2;

    1. 举例:a大于等于b
    2. new bigdemica(a).compareTo(new bigdemical(b)) >= 0

    常用使用时需注意如下整理的避坑点

    1.初始化创建对象使用new BigDecimal()还是BigDecimal#valueOf()

    先看下面这段代码

    1. BigDecimal bd1 = new BigDecimal(0.01);
    2. BigDecimal bd2 = BigDecimal.valueOf(0.01);
    3. System.out.println("bd1 = " + bd1);
    4. System.out.println("bd2 = " + bd2);

    输出到控制台的结果是:

    1. bd1 = 0.01000000000000000020816681711721685132943093776702880859375
    2. bd2 = 0.01

    造成这种差异的原因是0.1这个数字计算机是无法精确表示的,送给BigDecimal的时候就已经丢精度了,而BigDecimal#valueOf的实现却完全不同

    1. public static BigDecimal valueOf(double val) {
    2. // Reminder: a zero double returns '0.0', so we cannot fastpath
    3. // to use the constant ZERO. This might be important enough to
    4. // justify a factory approach, a cache, or a few private
    5. // constants, later.
    6. return new BigDecimal(Double.toString(val));
    7. }

    它使用了浮点数相应的字符串来构造BigDecimal对象,因此避免了精度问题。

    结论:所以大家要尽量要使用字符串而不是浮点数去构造BigDecimal对象,如果实在不行,就使用BigDecimal#valueOf()方法吧。

    2. 等值比较方法 使用equals还是compareTo ?

    1. BigDecimal bd1 = new BigDecimal("1.0");
    2. BigDecimal bd2 = new BigDecimal("1.00");
    3. System.out.println(bd1.equals(bd2));
    4. System.out.println(bd1.compareTo(bd2));

    控制台的输出将会是:

    1. false
    2. 0

    结论:使用compareTo方法究其原因是,BigDecimalequals方法的实现会比较两个数字的精度,而compareTo方法则只会比较数值的大小。

    3.BigDecimal并不代表无限精度 除法保留精度问题,巧用舍入模式

    先看这段代码

    1. BigDecimal a = new BigDecimal("1.0");
    2. BigDecimal b = new BigDecimal("3.0");
    3. a.divide(b) // results in the following exception.

    结果会抛出异常:

    java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

    原因:关于这个异常,Oracle的官方文档有具体说明

    If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

    大意是,如果除法的商的结果是一个无限小数但是我们期望返回精确的结果,那程序就会抛出异常。回到我们的这个例子,我们需要告诉JVM我们不需要返回精确的结果就好了。

    正确使用方式:

    1. BigDecimal a = new BigDecimal("1.0");
    2. BigDecimal b = new BigDecimal("3.0");
    3. a.divide(b, 2, RoundingMode.HALF_UP)// 0.33

    注:RoundingMode类型可参看上述舍入模式说明,按需使用

    4.BigDecimal转回String类型要小心避坑

    1. BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
    2. String out = d.toString(); // Or perform any formatting that needs to be done
    3. System.out.println(out); // 1.23345353454567E+16

    可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果BigDecimal有三个方法可以转为相应的字符串类型,切记不要用错:

    正确使用方式:

    1. String toString(); // 有必要时使用科学计数法
    2. String toPlainString(); // 不使用科学计数法
    3. String toEngineeringString(); // 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数

    5.执行顺序不能调换(乘法交换律失效

    乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况!!

    1. BigDecimal a = BigDecimal.valueOf(1.0);
    2. BigDecimal b = BigDecimal.valueOf(3.0);
    3. BigDecimal c = BigDecimal.valueOf(3.0);
    4. System.out.println(a.divide(b, 2, RoundingMode.HALF_UP).multiply(c)); // 0.990
    5. System.out.println(a.multiply(c).divide(b, 2, RoundingMode.HALF_UP)); // 1.00

    别小看这这0.01的差别,在汇金领域,会产生非常大的金额差异!!

  • 相关阅读:
    【PAT甲级 - C++题解】1076 Forwards on Weibo
    【状态估计】无迹卡尔曼滤波(UKF)应用于FitzHugh-Nagumo神经元动力学研究(Matlab代码实现)
    JSP中的九大隐式对象
    uniapp 开发公众号 h5(openid,微信支付,订阅通知)
    Fluidd摄像头公网无法正常显示修复一例
    Elasticsearch 8.10 同义词管理新篇章:引入同义词 API
    embedding层的理解
    DSPE-PEG-Hydroxyl,DSPE-PEG-OH,一种18碳饱和磷脂PEG衍生物
    windows 定制 terminal 上手实践
    kubernetes中ingress控制器traefik获取真实客户源IP
  • 原文地址:https://blog.csdn.net/CoderTnT/article/details/126153784