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)若要变更精度规则,则只需修改一个地方。