BigDecimal二三事

概述

作为JAVA程序员,应该或多或少跟BigDecimal打过交道。JAVA在java.math包中供给的API类BigDecimal,用来对超过16位有用位的数进行准确的运算。

精度丢掉

先从1个问题说起,看如下代码

System.out.println(0.1 + 0.2);

最终打印出的成果是0.30000000000000004,而不是预期的0.3。
有经历的开发同学应该一下子看出来这便是由于double丢掉精度导致。更深层次的原因,是由于咱们的核算机底层是二进制的,只要0和1,对于整数来说,从低到高的每1位代表了1、2、4、8、16…这样的2的正次数幂,只要位数足够,每个整数都能够分解成这样的2的正次数幂组合,例如7D=111B13D=1101B。可是到了小数这儿,就会发现2的负次数幂值是0.5、0.25、0.125、0.0625这样的值,可是并不是每个小数都能够分解成这样的2的负次数幂组合,例如你无法准确凑出0.1。所以,double的0.1其实并不是准确的0.1,只是经过几个2的负次数幂值凑的近似的0.1,所以会呈现前面0.1 + 0.2 = 0.30000000000000004这样的成果。

适用场景

双精度浮点型变量double能够处理16位有用数,可是某些场景下,即便现已做到了16位有用位的数还是不行,比如触及金额核算,差一点就会导致账目不平。

常用办法

加减乘除

已然BigDecimal首要用于数值核算,那么最基础的办法便是加减乘除。BigDecimal没有对应的数值类的基本数据类型,所以不能直接运用+-*/这样的符号来进行核算,而要运用BigDecimal内部的办法。

public BigDecimal add(BigDecimal augend)
public BigDecimal subtract(BigDecimal subtrahend)
public BigDecimal multiply(BigDecimal multiplicand)
public BigDecimal divide(BigDecimal divisor)

需求注意的是,BigDecimal是不可变的,所以,addsubtractmultiplydivide办法都是有回来值的,回来值是一个新的BigDecimal目标,本来的BigDecimal值并没有变。

设置精度和舍入战略

能够经过setScale办法来设置精度和舍入战略。

public BigDecimal setScale(int newScale, RoundingMode roundingMode)

第1个参数newScale代表精度,即小数点后位数;第2个参数roundingMode代表舍入战略,RoundingMode是一个枚举,用来替代本来在BigDecimal界说的常量,本来在BigDecimal界说的常量现已标记为Deprecated。在RoundingMode类中也经过1个valueOf办法来给出映射关系

/**
 * Returns the {@code RoundingMode} object corresponding to a
 * legacy integer rounding mode constant in {@link BigDecimal}.
 *
 * @param  rm legacy integer rounding mode to convert
 * @return {@code RoundingMode} corresponding to the given integer.
 * @throws IllegalArgumentException integer is out of range
 */
public static RoundingMode valueOf(int rm) {
    return switch (rm) {
        case BigDecimal.ROUND_UP          -> UP;
        case BigDecimal.ROUND_DOWN        -> DOWN;
        case BigDecimal.ROUND_CEILING     -> CEILING;
        case BigDecimal.ROUND_FLOOR       -> FLOOR;
        case BigDecimal.ROUND_HALF_UP     -> HALF_UP;
        case BigDecimal.ROUND_HALF_DOWN   -> HALF_DOWN;
        case BigDecimal.ROUND_HALF_EVEN   -> HALF_EVEN;
        case BigDecimal.ROUND_UNNECESSARY -> UNNECESSARY;
        default -> throw new IllegalArgumentException("argument out of range");
    };
}

咱们逐一看一下每个值的含义

  • UP
    直接进位,例如下面代码成果是3.15
BigDecimal pi = BigDecimal.valueOf(3.141);
System.out.println(pi.setScale(2, RoundingMode.UP));
  • DOWN
    直接舍去,例如下面代码成果是3.1415
BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(4, RoundingMode.DOWN));
  • CEILING
    假如是正数,相当于UP;假如是负数,相当于DOWN。
  • FLOOR
    假如是正数,相当于DOWN;假如是负数,相当于UP。
  • HALF_UP
    便是咱们正常理解的四舍五入,实际上应该也是最常用的。 下面的代码成果是3.14
BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(2, RoundingMode.HALF_UP));

下面的代码成果是3.142

BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(3, RoundingMode.HALF_UP));
  • HALF_DOWN
    与四舍五入类似,这种是五舍六入。咱们对于HALF_UP和HALF_DOWN能够理解成对于5的处理不同,UP遇到5是进位处理,DOWN遇到5是舍去处理,
  • HALF_EVEN
    假如放弃部分左边的数字为偶数,相当于HALF_DOWN;假如放弃部分左边的数字为奇数,相当于HALF_UP
  • UNNECESSARY
    非必要舍入。假如除去小数的后导0后,位数小于等于scale,那么便是去除scale位数后面的后导0;位数大于scale,抛出ArithmeticException。
    下面代码成果是3.14
BigDecimal pi = BigDecimal.valueOf(3.1400);
System.out.println(pi.setScale(2, RoundingMode.UNNECESSARY));

下面代码抛出ArithmeticException

BigDecimal pi = BigDecimal.valueOf(3.1400);
System.out.println(pi.setScale(1, RoundingMode.UNNECESSARY));

常见问题

创立BigDecimal目标

先看下面代码

BigDecimal a = new BigDecimal(0.1);
System.out.println(a);

实际输出的成果是0.1000000000000000055511151231257827021181583404541015625。其实这跟咱们开篇引出的精度丢掉是同一个问题,这儿结构办法中的参数0.1是double类型,本身无法准确表示0.1,虽然BigDecimal并不会导致精度丢掉,可是在愈加上游的源头,double类型的0.1现已丢掉了精度,这儿用一个现已丢掉精度的0.1来创立不会丢掉精度的BigDecimal,精度还是会丢掉。类似于运用2K的清晰度重新录制了一遍原始只要360P的视频,清晰度也不会优于原始的360P。
所以,咱们应该尽量避免运用double来创立BigDecimal,确实源头是double的,咱们能够运用valueOf办法,这个办法会先调用Double.toString(val)来转成String,这样就不会产生精度丢掉,下面的代码成果便是0.1

BigDecimal a = BigDecimal.valueOf(0.1);
System.out.println(a);

顺便说一下,BigDecimal还内置了ZEROONETEN这样的常量能够直接运用。

toString

这个问题比较隐蔽,在数据比较小的时分不会遇到,可是看如下代码

BigDecimal a = BigDecimal.valueOf(987654321987654321.123456789123456789);
System.out.println(a);

最终实际输出的成果是9.8765432198765427E+17。原因是System.out.println会自动调用BigDecimal的toString办法,而这个办法会在必要时运用科学计数法,假如不想运用科学计数法,能够运用BigDecimal的toPlainString办法。别的提一下,BigDecimal还供给了一个toEngineeringString办法,这个办法也会运用科学技术法,不一样的是,这儿面的10都是3、6、9这样的幂,对应咱们在检查大数的时分,许多都是每3位会添加1个逗号。

comparTo 和 equals

这个问题呈现的不多,有经历的开发同学在比较数值的时分,会自然而然运用comparTo办法。这儿说一下BigDecimal的equals办法除了比较数值之外,还会比较scale精度,不同精度不会equles。
例如下面代码分别会回来0false

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.compareTo(b));
System.out.println(a.equals(b));

不能除尽时ArithmeticException反常

上面说到的加减乘除的4个办法中,除法会比较特别,由于可能呈现除不尽的状况,这时假如没有设置精度,就会抛出ArithmeticException,由于这个是否能除尽是跟详细数值相关的,这会导致偶现的bug,愈加难以排查。
例如下面代码就会抛出ArithmeticException反常

BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(3);
System.out.println(a.divide(b));

应对的办法是,在除法运算时,注意设置成果的精度和舍入模式,下面的代码就能正常输出成果0.33

BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(3);
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));

总结

BigDecimal首要用于double由于精度丢掉而不满足的某些特别业务场景,例如会计金额核算。在能够忍耐略微不准确的场景还是运用内部供给的addsubtractmultiplydivide办法来进行基础的加减乘除运算,运算后会回来新的目标,原始的目标并不会改动。在运用BigDecimal的过程中,要注意创立目标、toString、比较数值、不能除尽时需求设置精度等问题。

开启成长之旅!这是我参与「日新方案 2 月更文应战」的第 1 天,点击检查活动详情