前言: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对象。如图所示:
本文会重点介绍上图所标记的三种方法。我们先建立一个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。
可以参考:JDK1.6中文文档
二. 总结
对于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 + "元。所以购买维达更划算!");
}
}
}
下一篇:DecimalFormat的正确打开姿势
后续
注:该篇博客已经授权公众号“方志朋”原创,并发布于2019-09-03。
等着大佬的书~ ?