【BigDecimal】大的小数,你会了吗?

1,496 阅读8分钟

前言

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战0.3 - 0.1 = ? 有点脑子的都知道答案是 0.2 ,但是计算机没有脑子呀,不信你可以试试, F12 打开控制台,结果和在 Java 中输入:System.out.println(0.3 - 0.1); 打印结果是完全一样的,即 0.19..8 ,对于这种损失精度的问题如果出现在商业金额计算上后果将不堪设想。双精度浮点型变量 double 可以处理 16 位有效数,但是在实际应用中,需要对更大或者更小的数进行运算和处理,为避免精度缺失,让你在小数/大数计算中得到精确的预期值,这时就需要创建 BigDecimal 对象用来对超过 16 位有效位的数进行精确的运算。

创建 BigDecimal 对象

将值放入 BigDecimal 构造器中即可创建一个对象,具体入参列表如下:

image.png

对于 MathContext 对象和 scale 标度下面会讲到。

采用不同的入参类型进行构造,出现的损失精度问题

注意: 通常建议采用 String 类型入参进行构造,否则会和预期结果不一致。

举个栗子:

// 入参类型选择不当,出现的损失精度问题
BigDecimal onedotone1 = new BigDecimal(1.1);
System.out.println(onedotone1); // 1.100000000000000088817841970012523233890533447265625
BigDecimal onedotone2 = new BigDecimal("1.1");
System.out.println(onedotone2); // 1.1

double doubleNum = 3.14D;
BigDecimal no = new BigDecimal(doubleNum);
System.out.println(no.doubleValue()); // 3.14
BigDecimal yes1 = new BigDecimal(Double.toString(doubleNum));
System.out.println(yes1.doubleValue()); // 3.14
// or
BigDecimal yes2 = BigDecimal.valueOf(1.1D);
System.out.println(yes2.doubleValue()); // 3.14

System.out.println("Double 类型小数入参时,错误写法构造出来的结果:" + no.toString() + ",与预期相比:" + (Double.toString(doubleNum).equals(no.toString()) ? "一样": "不一样"));
// Double 类型小数入参时,错误写法构造出来的结果:3.140000000000000124344978758017532527446746826171875,与预期相比:不一样
System.out.println("Double 类型小数入参时,正确写法构造出来的结果:" + yes.toString() + ",与预期大小判断:" + (Double.toString(doubleNum).equals(yes.toString()) ? "一样": "不一样"));
// Double 类型小数入参时,正确写法构造出来的结果:3.14,与预期大小判断:一样

使用 new BigDecimal(double val) 这个构造器初始化的结果可能有些 不可预测 ,传入的小数无法准确地表示为 double,也就是 该小数在计算机中无法精确表示 。虽然好像是帮我们构造了一个和预期值一致的 BigDecimal 对象,但是 toString() => 1.100000000000000088817841970012523233890533447265625doubleValue() => 得到的才是预期值。

而使用 new BigDecimal(String val) 这个构造器初始化的结果是 完全可预测的 ,传入的小数字符串准确地等于预期值,所以比较而言,通常建议采用 String 类型入参进行构造,或者入参类型为 long / double 使用 BigDecimal.valueOf() 进行构造。

舍入模式 [ Rounding Mode ]

scale

BigDecimal 中有一个非常重要的概念 —— scale 标度,那它到底表示什么?

  • scale >= 0 表示这个数字小数点右侧的位数

  • scale < 0 表示 value * (10 ^ |scale|) => 预期值 value × 10scale 绝对值次方

举例:

[unscaled value, scale]The resulting string
[123, 0]"123"
[-123, 0]"-123"
[123, -1]"1.23E+3"
[123, -3]"1.23E+5"
[123, 1]"12.3"
[123, 5]"0.00123"
[123, 10]"1.23E-8"
[-123, 12]"-1.23E-10"

除了对 unscaled value 指定标度值外,还可以指定 RoundingMode 舍入模式,也就是大家常说的 四舍五入五舍六入 等一些保留位数策略。

RoundingMode.HALF_UP 四舍五入

// 初始化对象并设置标度和指定舍入模式
double result1 = BigDecimal.valueOf(1.356).setScale(2, RoundingMode.HALF_UP).doubleValue();
System.out.println("正数四舍五入:" + result1); // 1.36

double result2 = BigDecimal.valueOf(-1.356).setScale(2, RoundingMode.HALF_UP).doubleValue();
// 负数先取绝对值再四舍五入,四舍五入后前添负号变负数
System.out.println("负数四舍五入:" + result2); // -1.36

RoundingMode.HALF_DOWN 五舍六入

double result1 = BigDecimal.valueOf(1.357).setScale(2, RoundingMode.HALF_DOWN).doubleValue();
System.out.println("正数五舍六入:" + result1); // 1.36

double result2 = BigDecimal.valueOf(-1.355).setScale(2, RoundingMode.HALF_DOWN).doubleValue();
// 负数先取绝对值再五舍六入,五舍六入后前添负号变负数
System.out.println("负数五舍六入:" + result2); // -1.35

RoundingMode.HALF_EVEN 偶五舍奇五入

double result1 = BigDecimal.valueOf(20.245).setScale(2, RoundingMode.HALF_EVEN).doubleValue();
System.out.println("保留的位数是偶数,五舍六入:" + result1); // 20.24

double result2 = BigDecimal.valueOf(20.2635).setScale(3, RoundingMode.HALF_EVEN).doubleValue();
// 首先判断保留位数的奇偶性,奇数四舍五入
System.out.println("保留的位数是奇数,四舍五入:" + result2); // 20.264

RoundingMode.UP 远离零的方向舍入

double result1 = BigDecimal.valueOf(1.23456789).setScale(3, RoundingMode.UP).doubleValue();
System.out.println("(正数)远离零的方向舍入:" + result1); // 1.235

double result2 = BigDecimal.valueOf(-1.23456789).setScale(3, RoundingMode.UP).doubleValue();
System.out.println("(负数)远离零的方向舍入:" + result2); // -1.235

RoundingMode.DOWN 向零的方向舍入

double result1 = BigDecimal.valueOf(20.249999).setScale(2, RoundingMode.DOWN).doubleValue();
System.out.println("(正数)向零的方向舍入,即舍去保留位数后面的所有位:" + result1); // 20.24

double result2 = BigDecimal.valueOf(-20.249999999).setScale(2, RoundingMode.DOWN).doubleValue();
System.out.println("(负数)向零的方向舍入,即舍去保留位数后面的所有位:" + result2); // -20.24

RoundingMode.CEILING 天花板舍入

double result1 = BigDecimal.valueOf(1.356).setScale(2, RoundingMode.CEILING).doubleValue();
System.out.println("(正数)天花板舍入:" + result1); // 1.36

double result2 = BigDecimal.valueOf(-1.356).setScale(2, RoundingMode.CEILING).doubleValue();
// 天花板顾名思义,更高、更大的一方,-1.35 > -1.356,而不是 -1.36 < -1.356
System.out.println("(负数)天花板舍入:" + result2); // -1.35

RoundingMode.FLOOR 地板舍入

double result1 = BigDecimal.valueOf(1.356).setScale(2, RoundingMode.FLOOR).doubleValue();
System.out.println("正数地板舍入:" + result1); // 1.35

double result2 = BigDecimal.valueOf(-1.356).setScale(2, RoundingMode.FLOOR).doubleValue();
// 地板顾名思义,更低、更小的一方,-1.36 < -1.356,而不是 -1.35 > -1.356
System.out.println("负数地板舍入:" + result2); // -1.36

注意: 舍入模式不会增加计算值的大小。

总结:

  1. Rounding Mode.HALF_UP:四舍五入,负数先取绝对值再四舍五入,四舍五入后前添负号变负数

  2. RoundingMode.HALF_DOWN:五舍六入,负数先取绝对值再五舍六入,五舍六入后前添负号变负数

  3. RoundingMode.HALF_EVEN:保留位数是偶数,五舍六入;保留位数是奇数,四舍五入

  4. RoundingMode.UP:向远离原点(零)的方向舍入,即保留的位数的同时,末位加 1

  5. RoundingMode.DOWN:向原点(零)的方向舍入,即除了保留的位数外全部舍去

  6. RoundingMode.CEILING:天花板舍入,取更大值

  7. RoundingMode.FLOOR:地板舍入,取更小值

常用静态方法

创建 BigDecimal 对象无非就是对值的进行加减乘除等一些数学运算:

BigDecimal.add(augend)

// 被加数
BigDecimal addend = new BigDecimal("100");
// 加数
BigDecimal augend = BigDecimal.valueOf(32.123312D);

// 和 = 加数 + 被加数
BigDecimal sum = addend.add(augend);
System.out.println("相加之和:" + sum); // 132.123312

除此之外,还能对计算结果指定有效数字位数,也就是设置保留的精度(Precision)以及舍入模式(DEFAULT_ROUNDINGMODE = RoundingMode.HALF_UP),通过传入 MathContext 对象作为参数即可。

// MathContext 指定有效数字位数(精度)以及舍入策略
MathContext mathContext = new MathContext(5, RoundingMode.HALF_UP);
// 等价于 new MathContext(5);
BigDecimal sum = addend.add(augend, mathContext);
System.out.println("传入 MathContext 参数去指定计算结果的有效数字位数以及舍入策略:" + sum); // // 132.12

BigDecimal.subtract(subtrahend)

// 被减数
BigDecimal minuend = new BigDecimal("100");
// 减数
BigDecimal subtrahend = BigDecimal.valueOf(32.123312D);

// 差 = 被减数 - 减数
BigDecimal subtractResult = minuend.subtract(subtrahend);
System.out.println("相减之差:" + subtractResult); // 67.876688

Bigdecimal.multiply(multiplicand)

// 被乘数
BigDecimal multiplicand = new BigDecimal("100");
// 乘数
BigDecimal multiplier = BigDecimal.valueOf(32.123312D);

// 积 = 乘数 × 被乘数
BigDecimal multiplyResult = multiplier.multiply(multiplier);
System.out.println("相乘之积:" + multiplyResult); // 3212.331200

BigDecimal.divide(divisor)

image.png

// 被除数
BigDecimal dividend = new BigDecimal("4.5");
// 除数
BigDecimal divisor = BigDecimal.valueOf(1.3D);

// 商 = 被除数 ÷ 除数
BigDecimal quotient = dividend.divide(divisor);
System.out.println("相除之商:" + quotient); //  throws exception

注意: 4.5/1.3 两数不整除时就会报错:java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

那如何解决除不尽的问题呢?

很简单,除不尽截断就好了,只保留整数或者保留指定有效位数:

// 方法一:保留小数点后两位,对第三位四舍五入
dividend.divide(divisor, 2, RoundingMode.HALF_UP);

// 方法二:设置保留精度,默认采取四舍五入模式
dividend.divide(divisor, new MathContext(3));

// 方法三:商取整
dividend.divideToIntegralValue(divisor)

BigDecimal.divideAndRemainder(divisor)

BigDecimal[] remainder = dividend.divideAndRemainder(divisor);

System.out.println("保留整数位:" + remainder[0]); // 3
// remainder[0] <=> dividend.divideToIntegralValue(divisor)
System.out.println("取余数:" + remainder[1]); // 0.6
// remainder[1] <=> dividend.remainder(divisor)

最终返回的结果是一个双元素数组,其中包含 divideToIntegralValue 的结果后面跟着的 remainder 的结果。

如果你需要一个整数商和其余数,用这个方法比分别用对应的方法效率更快,因为除法只需要执行一次。

BigDecimal.movePointLeft(n) | BigDecimal.movePointRight(n)

将小数点,向左/右移动 n 位,返回移动后的数值。

// 100 * 10^-2 = 1.00
System.out.println("小数点向左移动两位:" + new BigDecimal(100).movePointLeft(2));

// 100 * 10^3 = 100000
System.out.println("小数点向右移动三位:" + new BigDecimal(100).movePointRight(3));

BigDecimal.precision()

获取 BigDecimal 对象中初始值的精度。

System.out.println(new BigDecimal("3.14158").precision()); // 6
System.out.println(new BigDecimal("123456").precision()); // 6

BigDecimal.pow(n)

返回 BigDecimal 对象中初始值的 n 次方。

System.out.println(new BigDecimal(100).pow(2)); // 100^2 = 10000

BigDecimal.abs()

返回一个值为绝对值的 BigDecimal

System.out.println(new BigDecimal("-100").abs()); // |-100| = 100

BigDecimal.signum()

返回 -101,分别代表负数正数

System.out.println(new BigDecimal("100").signum()); // 1
System.out.println(new BigDecimal("0").signum()); // 0
System.out.println(new BigDecimal("-100").signum()); // -1

BigDecimal.negate()

返回一个值加负号的 BigDecimal

System.out.println(new BigDecimal("100").negate()); // 100 -> -100
System.out.println(new BigDecimal("-200").negate()); // -200 -> -(-200) = 200

BigDecimal.compareTo(val)

两个 BigDecimal 类型的数作比较,左边大于右边返回 1;小于返回 -1;相等则返回 0

System.out.println(new BigDecimal("100").compareTo(new BigDecimal("200"))); // -1
System.out.println(new BigDecimal("100").compareTo(new BigDecimal("100"))); // 0 
System.out.println(new BigDecimal("100").compareTo(BigDecimal.ZERO)); // 1

BigDecimal.round(mathContext)

设置有效位数,根据舍入模式进行取舍。

MathContext mathContext = new MathContext(4, RoundingMode.DOWN);
System.out.println(new BigDecimal("3.1415926").round(mathContext)); // 3.141

使用场景

一个省份的邀请人数占入驻总人数的百分之几,保留到小数点后两位,对最终结果四舍五入。

// 邀请人数
BigDecimal inviteNum = new BigDecimal("15");

BigDecimal totalNum = new BigDecimal("74");

// 建立百分比格式化引用
NumberFormat percent = NumberFormat.getPercentInstance(Locale.CHINA);
// 百分比保留最大位数
percent.setMaximumFractionDigits(2);

// 将两个 BigDecimal 的数相除,结果保留小数点后面的后四位,并采取四舍五入的策略
BigDecimal quotient = inviteNum.divide(totalNum, 4, RoundingMode.HALF_UP);

// 结果转换为百分比格式
String proportion = percent.format(quotient);
System.out.println(proportion); // 20.27%

对购物车中价格抹零取整(保留小数点后两位),抹零后返回货币格式的价格列表,清空购物车后返回账户余额,余额精确到分。


/**
 * 
 * @param sales 未货币化的价格列表
 * @param locale 货币格式
 * @return 返回指定货币格式并抹零取整后的价格列表
 */
private List<String> formatSale(List<Double> sales, Locale locale) {
    NumberFormat currency = NumberFormat.getCurrencyInstance(locale);
    DecimalFormat decimalFormat = new DecimalFormat("#");
    if (sales != null && sales.size() > 0) {
        return sales.stream()
                .map(sale -> {
                    String format = decimalFormat.format(sale);
                    return currency.format(Double.valueOf(format));
                }).collect(Collectors.toList());
    }
    return null;
}

// 账户余额
BigDecimal account = BigDecimal.valueOf(100.36D);
// 货币格式
Locale locale = Locale.CHINA;
// 购物车商品价格
List<Double> priceList = Arrays.asList(13.14D, 48.88D, 99.99D, 65.78D, 88.78D);

// 清空购物车,返回下单后的账户余额
Optional.ofNullable(formatSale(priceList, locale))
        .ifPresent(sales -> {
            // 购物车商品总价钱
            BigDecimal totalPrice = sales.stream()
                    .map(sale -> {
                        try {
                            return BigDecimal.valueOf(new DecimalFormat("¥##.##").parse(sale).doubleValue());
                        } catch (ParseException e) {
                            System.err.println(e.getMessage());
                            return BigDecimal.ZERO;
                        }
                    }).reduce(BigDecimal.ZERO, BigDecimal::add);
            // 清空购物车后余额
            BigDecimal balance = account.subtract(totalPrice);
            System.out.println(balance.compareTo(BigDecimal.ZERO) >=0 ? "清空购物车后,余额为:" + formatSale(Arrays.asList(balance.doubleValue()), locale) : "下个月吃土吧!"); // 下个月吃土吧!
        });

注意: 在解析货币格式的价格时不能使用 NumberFormat 抽象基类进行解析,那是不能被实例化的,可以用它的子类 DecimalFormat 根据价格的货币格式将字符串转化成双精度浮点型价格。

image.png

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。