好好学Java:从零基础到项目实战
上QQ阅读APP看书,第一时间看更新

4.3.2 大小数BigDecimal

BigInteger只能表达任意整数,但不能表达小数,要想表达任意小数,还需专门的大小数类型BigDecimal。如果说设计BigInteger的目的是替代int和long类型,那么设计BigDecimal的目的便是替代浮点型float和双精度型double。正如它的兄弟BigInteger一般,BigDecimal不存在数值范围限制,无论是整数部分还是小数部分,只要你能写得出来,BigDecimal就能表达出来,从此不必担心基本数字类型的精度问题了。

既然同为大数字家族,BigDecimal的绝大部分用法就与BigInteger保持一致,像add方法、subtract方法、abs方法、pow方法等直接拿来用便是,这里不再啰唆了。下面来看BigDecimal的方法调用代码(完整代码见本章源码的src\com\method\big\TestDecimal.java):

        BigDecimal sevenAndHalf=BigDecimal.valueOf(7.5);  // 生成一个指定数值的大小数变量
        BigDecimal three=BigDecimal.valueOf(3);  // 生成一个指定数值的大小数变量
        BigDecimal sum=sevenAndHalf.add(three);  // add方法用来替代加法运算符“+”
        System.out.println("sum="+sum);
        BigDecimal sub=sevenAndHalf.subtract(three);  // subtract方法用来替代减法运算符“-”
        System.out.println("sub="+sub);
        BigDecimal mul=sevenAndHalf.multiply(three);  // multiply方法用来替代乘法运算符“*”
        System.out.println("mul="+mul);
        BigDecimal div=sevenAndHalf.divide(three);  // divide方法用来替代除法运算符“/”
        System.out.println("div="+div);
        BigDecimal remainder=sevenAndHalf.remainder(three);  // remainder方法用来替代取余数运算符“%”
        System.out.println("remainder="+remainder);
        BigDecimal neg=sevenAndHalf.negate();  // negate方法用来替代负号运算符“-”
        System.out.println("neg="+neg);
        BigDecimal abs=sevenAndHalf.abs();  // abs方法用来替代数学库函数Math.abs
        System.out.println("abs="+abs);
        BigDecimal pow=sevenAndHalf.pow(2);  // pow方法用来替代数学库函数Math.pow
        System.out.println("pow="+pow);

难道这么容易就学会使用BigDecimal了吗?仔细看上面的代码,被除数是7.5,除数是3,二者相除得到的商为2.5。注意这是除得尽的情况,倘若换成除不尽的情况,例如把除数改成7,计算7.5除以7,结果理应得到一个无限循环小数。但是运行以下的测试代码,没想到程序竟然运行异常,未能打印那个值为无限循环小数的商:

        // 只有一个输入参数的divide方法,要求被除数能够被除数除得尽
        // 倘若除不尽,也就是商为无限循环小数,则程序会异常退出
        // 报错“Non-terminating decimal expansion; no exact representable decimal result.”
        BigDecimal seven=BigDecimal.valueOf(7);
        BigDecimal divTest=sevenAndHalf.divide(seven);
        System.out.println("divTest="+divTest);

虽说大小数能够表示任意范围的小数,但必须是一个有限的范围,而不能是无限的范围。由于内存容量是有限的,一个无限循环小数写出来都写不完,如果放到内存中就需要无限大小的内存,因此为了让内存能够放得下无限循环小数,只好给该小数指定需要保留的小数位数,也就意味着BigDecimal表示无限循环小数时还是有精度要求的。

除了规定小数部分的保留位数外,还需明确多余部分的数字是直接舍弃还是四舍五入。这样对于无限循环小数来说,除法运算的divide方法需要3个输入参数,包括除数、需要保留的小数位数和多余数字的舍入规则。BigDecimal提供的数字舍入规则见表4-2。

表4-2 大小数类型的数字舍入规则

由上述规则可知,通常情况下的四舍五入应当采取ROUND_HALF_UP方式。于是重新指定了小数精度和舍入规则,改写后大小数的除法运算代码示例如下:

        BigDecimal one=BigDecimal.valueOf(100);
        BigDecimal three=BigDecimal.valueOf(3);
        // 大小数的除法运算,小数点后面保留64位,其中最后一位四舍五入
        BigDecimal div=one.divide(three, 64, BigDecimal.ROUND_HALF_UP);
        System.out.println("div="+div);

运行修改后的除法代码,控制台打印的日志结果如下:

div=33.3333333333333333333333333333333333333333333333333333333333333333

可见此时除法计算正常工作,并且结果值的小数部分确实保留到了64位。

上述带3个输入参数的divide方法固然实现了符合精度的除法运算,但若代码存在多处调用divide方法,则意味着该方法后面的两个精度参数“64, BigDecimal.ROUND_HALF_UP”在每处调用的地方都会出现,这样不但造成代码重复,而且在变更精度规则时还得改动多处。为此,Java又提供了精度工具MathContext,利用该工具可以事先指定包含小数精度和舍入规则在内的精度规则,然后把设置好的工具实例传给divide方法就好了。下面是使用MathContext工具辅助除法运算的代码例子:

        // 利用工具MathContext可以把divide方法的输入参数减少为两个
        MathContext mc=new MathContext(64, RoundingMode.HALF_UP);
        BigDecimal divByMC=one.divide(three, mc);  // 根据指定的精度规则执行除法运算
        System.out.println("divByMC="+divByMC);

在大小数的除法中引入精度工具MathContext至少有以下两个好处:

(1)精度规则只要定义一次,即可多处使用。

(2)若要变更精度规则,则只需修改一个地方。