BigDecimal使用指南

2019/09/01

前言:BigDecimal的产生背景

在Java的8种基本类型中,我们知道double和float基本数据类型存在着精度缺失问题。我们先来看一个例子。

        double augend = 1.0000001;
        double addend = 0.0000001;
        double sum = augend + addend;
        //sum: 1.0000002000000001
        System.out.println("sum: " + sum);

可以看到sum并不是我们所想要的1.0000002,而是1.0000002000000001。虽然误差非常小,但是如果开发银行金融类产品时,我们就需要绝对精确的数据。试想下这样一个场景,在一个用户余额为99.95元时,恰好他要购买总价99.95元的商品,而程序中定义总价时使用了double类型,这就有可能使得总价大于余额,导致用户无法购买商品。不过,Java提供了BigDecimal类用来了解决此类问题。

一. 如何使用BigDecimal

本文将从一个实际生活场景出发——超市购物,通过在买单时计算总消费金额来介绍BigDecimal的基本使用方法(加减乘除等)。由这个生活中的具体例子出发,加深我们对BigDecimal的记忆和理解。

1.1 初始化BigDecimal对象

BigDecimal的源码中有16个构造函数和3个静态方法可以用于初始化BigDecimal对象。如图所示: 初始化BigDecimal对象的构造函数和静态方法 本文会重点介绍上图所标记的三种方法。我们先建立一个Receipt类,在该类中定义四个常量并使用main方法将它们打印出来。 在这里插入图片描述 从控制台的输出结果中,我们可以看到使用参数类型为String类型的构造函数创建的MEAT_PRICE对象打印结果为19.98,而使用参数类型为double类型的构造函数创建的RICE_PRICE对象打印结果为3.3300000000000000710542735760100185871124267578125(每个机器可能不一样)。当我们的数量足够多的时候,就会产生导致总价产生较大的误差,这是我们不希望看到的。同时,在使用静态方法valueOf的时候,也能得到精确的数字。这是因为源码中调用了Double类型的toString方法,然后再调用了参数类型为String类型的构造函数创建该对象。

1.2 BigDecimal的加减乘除等实例方法

在进行下面的操作之前,我们将RICE_PRICE改为使用valueOf方法初始化。我们在main方法中定义了double类型的weightOfMeat、weightOfRice变量和int类型的quantityOfVinda变量。同时,我们定义了一张面值100购物卡,为了方便演示,只是定义BigDecimal的amount对象,并且利用BigDecimal的常量ZERO创建totalPrice对象。 在这里插入图片描述 在使用BigDecimal的实例方法时,如multiply(乘法)方法,需要传入BigDecimal类型参数。同样,在加减和除的方法中,也是需要传入BigDecimal类型参数。所以在进行计算前,我们调用了valueOf类方法,用以获取对应数值的BigDecimal对象。从上图中,我们可以看出“猪肉总价为:44.9550”。虽然计算结果正确,但是却不符合我们实际生活需求。因为在超市的小票上,数字只有两位小数,所以我们在计算riceTotalPrice时,调用了实例方法setScale,该方法一共有三个重载方法,最常用的就是我们所使用的这种。在设置了小数点位数和舍入模式后,我们可以看到“大米总价为:84.42”,84.42才符合我们的实际需求。 接下来,我们将通过BigDecimal的add(加法)计算出totalPrice,再通过BigDecimal的subtract(减法)计算amount减去totalPrice,获得BigDecimal的differencePrice(差价)对象。通过BigDecimal的compareTo方法比较difference与BigDecimal.ZERO的大小关系,得出客户使用了购物卡后是否还需再付钱,如还要付钱,将调用BigDecimal的abs方法将difference取绝对值。 在这里插入图片描述 在这一次的购物的过程中,我们学会了BigDecimal的初始化方法,加减法,乘法和compareTo方法。不过,在比较两个值是否相等还有equals方法,这个方法要谨慎使用。因为这个equals方法不仅要比较数值是否相同,还要比较精度是否相同。也就是说,如果2.0和2.00用equals方法进行比较,返回的结果为false。到目前为止,还有一个常用的方法——divide(除法),我们没学到。现在,我们就来使用divide算一算是买清风卷纸划算还是买维达卷纸划算。 在这里插入图片描述 通过使用divide方法,我们计算出了qingFengSingle和vindaSingle。在调用divide的过程中,我们指定了保留的小数点位数和舍入模式。在BIgDecimal中divide一共有6个重载方法,其形参不同之处在于是否有精度参数,是否有舍入模式参数及舍入模式参数的类型。

1.3 八种舍入模式

在上面的代码中,我们在为setScale或divide指定舍入模式参数时,是通过RoundingMode的枚举指定的。RoundingMode的枚举对象一共有8个,分别为:RoundingMode.UP、RoundingMode.DOWN、RoundingMode.CEILING、RoundingMode.FLOOR、RoundingMode.HALF_UP、RoundingMode.HALF_DOWN、RoundingMode.HALF_EVEN和RoundingMode.UNNECESSARY。RoundingMode枚举类有一个类型为int的oldModel变量,从0-7依次对应上面8种枚举对象。关于枚举类型的简单介绍,可参考博主的《JAVA枚举类型(Enum)的使用》

  • RoundingMode.UP:舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。
  • RoundingMode.DOWN:接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。
  • RoundingMode.CEILING:接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同;如果为负,则舍入行为与 ROUND_DOWN 相同。注意,此舍入模式始终不会减少计算值。
  • RoundingMode.FLOOR:接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同;如果为负,则舍入行为与 ROUND_UP 相同。注意,此舍入模式始终不会增加计算值。
  • RoundingMode.HALF_UP:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们在小学时学过的舍入模式(四舍五入)。
  • RoundingMode.HALF_DOWN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。
  • RoundingMode.HALF_EVEN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 ==> 1.2 ,1.25 ==> 1.2
  • RoundingMode.UNNECESSARY:断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

总结

对于BigDecimal类,我们已经掌握了基本的用法,这些用法也是开发过程中最常见的用法。BigDecimal是可以绝对控制小数点位数的一种数据类型,它适用于银行金融,商城类业务开发。同时,作为一种数据类型,其最根本的功能就是对数据进行运算,这也是本文的核心内容。 参考网址:BigDecimal加减乘除计算廖雪峰的官方网站。 代码:

public class Receipt {

    //猪肉单价(单位:kg)
    private static final BigDecimal MEAT_PRICE = new BigDecimal("19.98");
    //大米单价(单位:kg)
    private static final BigDecimal RICE_PRICE = new BigDecimal(3.33D);
    //清风卷纸(12包装)单价
    private static final BigDecimal QINGFENG_ROLL_PAPER_PRICE = BigDecimal.valueOf(25D);
    //维达卷纸(20包装)单价
    private static final BigDecimal VINDA_ROLL_PAPER_PRICE = BigDecimal.valueOf(50D);

    public static void main(String[] args) {
        //购物卡余额:100
        BigDecimal amount = BigDecimal.valueOf(100D);
        //总价初始化为0
        BigDecimal totalPrice = BigDecimal.ZERO;
        //猪肉净含量
        double weightOfMeat = 2.25D;
        //大米净含量
        double weightOfRice = 25.35D;
        //卷纸数量
        int quantityOfVinda = 1;

        //猪肉总价:44.9550
        BigDecimal meatTotalPrice = MEAT_PRICE.multiply(BigDecimal.valueOf(weightOfMeat)).setScale(2, RoundingMode.HALF_UP);
        //大米总价:84.42
        // 3.33 * 25.35 = 84.4155,通过setScale设置保留的2位小数,并且设置舍入模式为四舍五入
        BigDecimal riceTotalPrice = RICE_PRICE.multiply(BigDecimal.valueOf(weightOfRice)).setScale(2, RoundingMode.HALF_UP);
        //维达卷纸总价:50.00
        BigDecimal vindaRollPaperPrice = VINDA_ROLL_PAPER_PRICE.multiply(BigDecimal.valueOf(quantityOfVinda)).setScale(2, RoundingMode.HALF_UP);

        //通过add(加法)计算总价totalPrice:179.38
        totalPrice = totalPrice.add(meatTotalPrice).add(riceTotalPrice).add(vindaRollPaperPrice);
        //通过subtract(减法)计算差价differencePrice:-79.38
        BigDecimal differencePrice = amount.subtract(totalPrice);
        if (differencePrice.compareTo(BigDecimal.ZERO) < 0) {
            System.out.println("请付款:" + differencePrice.abs() + "元," + "购物卡余额:0元.");
        }else{
            System.out.println("购物卡余额:" + differencePrice + "元.");
        }

        //通过divide(除法)计算一包清风纸的价格,设置采用四舍五入模式保留2位小数,并使用doubleValue方法将结果转化成double类型
        double qingFengSingle = QINGFENG_ROLL_PAPER_PRICE.divide(BigDecimal.valueOf(12),2, RoundingMode.HALF_UP).doubleValue();
        //通过divide(除法)计算一包维达纸的价格,
        double vindaSingle = VINDA_ROLL_PAPER_PRICE.divide(BigDecimal.valueOf(20),2, RoundingMode.HALF_UP).doubleValue();
        int result = Double.compare(qingFengSingle, vindaSingle);

        if (result < 0) {
            System.out.println("一包清风纸价钱" + qingFengSingle + "元,小于一包维达纸价钱" + vindaSingle + "元。所以购买清风更划算!");
        }else if (result == 0){
            System.out.println("一包清风纸价钱" + qingFengSingle + "元,等于一包维达纸价钱" + vindaSingle );
        }else {
            System.out.println("一包清风纸价钱" + qingFengSingle + "元,大于一包维达纸价钱" + vindaSingle + "元。所以购买维达更划算!");
        }
    }
}
本文为原创文章,转载请标明出处。
本文链接:https://www.fangzhipeng.com/java/2019/09/01/use-bigdemecial.html
本文出自方志朋的博客


扫码关注公众号送888G架构师资料

(转载本站文章请注明作者和出处 方志朋-forezp

宝剑锋从磨砺出,梅花香自苦寒来,用心分享,一起成长,做有温度的攻城狮!

Post Directory