使用BigDecimal进行精确数字运算

我们都知道在 Java 中常用的浮点数类型有 float 和 double 这两种,但是其实这两种类型的数据并无法精确表示运算结果。先看一个例子:

public class PrecisionTest {
	public static void main(String[] args) {
		System.out.println(0.03 * 0.045);
		System.out.println(0.003 - 0.013);
		System.out.println(0.21 / 0.07);
	}
}
输出结果为:
0.0013499999999999999
-0.009999999999999998
2.9999999999999996

可以看到这些结果并不是我们所期待的那样,都存在着很微小的误差。因为计算机中的所有数据都是二进制存储的,对于数字来说,由于二进制本身的限制,所以表示小数的时候是不可能做到完全精确的,只能人为地进行精度的取舍才能做到尽量精确。

就我个人思考而言,其实 float 和 double 也是可以做到所在精度范围内的精确,只是 Java 语言设计者在设计的时候为此专门设计了一个 java.math.BigDecimal 类,所以 float 和 double 就没有必要也做到那么精确。

在很多资料中看到 float 和 double 只能用来做科学计算或者是工程计算,要想进行商业计算就得用 java.math.BigDecimal 类了。其实稍加思考就能理解。普通的数学计算对于精度来讲是允许有一定的误差的,只要在误差范围之内;但是谈到钱的话我估计没人会希望有误差吧,就算用户允许自己的钱存在误差,但是作为商业机构来讲,它当然是希望计算更加精确,不仅更加方便管理也能减少一些不必要的麻烦。

那么我们就来看一看 BigDecimal 类和普通的 float 和 double 数据类型在精确性上的区别:

import java.math.BigDecimal;
import java.math.MathContext;

public class BigDecimalArithmetic {
	public static void main(String[] args) {
		double v1 = 0.03;
		double v2 = 0.045;

		double addResult = add(v1, v2);
		double subResult = subtract(v1, v2);
		double mulResult = multiply(v1, v2);
		double divResult = divide(v1, v2);

		System.out.println("+++++++ 未使用BigDecimal: +++++++");
		System.out.println(v1 + " + " + v2 + " = " + (v1 + v2));
		System.out.println(v1 + " - " + v2 + " = " + (v1 - v2));
		System.out.println(v1 + " * " + v2 + " = " + (v1 * v2));
		System.out.println(v1 + " / " + v2 + " = " + (v1 / v2));

		System.out.println("\n+++++++ 使用BigDecimal: +++++++");
		System.out.println(v1 + " + " + v2 + " = " + addResult);
		System.out.println(v1 + " - " + v2 + " = " + subResult);
		System.out.println(v1 + " * " + v2 + " = " + mulResult);
		System.out.println(v1 + " / " + v2 + " = " + divResult);
	}

	 /**
	 * 防止被实例化
	 */
	 private BigDecimalArithmetic() {
	 }

	/**
	 * 精确的加法计算
	 * 
	 * @param v1
	 *            被加数
	 * @param v2
	 *            加数
	 * @return 和
	 */
	public static double add(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));

		return b1.add(b2).doubleValue();
	}

	/**
	 * 精确的减法计算
	 * 
	 * @param v1
	 *            被减数
	 * @param v2
	 *            减数
	 * @return 差
	 */
	public static double subtract(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));

		return b1.subtract(b2).doubleValue();
	}

	/**
	 * 精确的乘法运算
	 * 
	 * @param v1
	 *            被乘数
	 * @param v2
	 *            乘数
	 * @return 积
	 */
	public static double multiply(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));

		return b1.multiply(b2).doubleValue();
		// return b1.multiply(b2, new MathContext(4)).doubleValue();
	}

	/**
	 * 精确的除法运算
	 * 
	 * @param v1
	 *            被除数
	 * @param v2
	 *            除数
	 * @param scale
	 *            精度
	 * @return 商
	 */
	public static double divide(double v1, double v2, int scale) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));

		return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
		// return b1.divide(b2, new MathContext(4)).doubleValue();

	}

	/**
	 * 按照默认精确位数进行的除法计算
	 * 
	 * @param v1
	 *            被除数
	 * @param v2
	 *            除数
	 * @return 商
	 */
	public static double divide(double v1, double v2) {
		return divide(v1, v2, DEFAULT_SCALE);
	}

	/**
	 * 精确的舍入处理
	 * 
	 * @param v
	 *            待舍入的值
	 * @param scale
	 *            精确位数
	 * @return 精确舍入后的值
	 */
	public static double round(double v, int scale) {
		BigDecimal b1 = new BigDecimal(Double.toString(v));
		BigDecimal b2 = new BigDecimal("1");

		return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
	}

	private static final int DEFAULT_SCALE = 10; // 默认精度,即小数点后的位数
}
输出结果为:
+++++++ 未使用BigDecimal: +++++++
0.03 + 0.045 = 0.075
0.03 - 0.045 = -0.015
0.03 * 0.045 = 0.0013499999999999999
0.03 / 0.045 = 0.6666666666666666

+++++++ 使用BigDecimal: +++++++
0.03 + 0.045 = 0.075
0.03 - 0.045 = -0.015
0.03 * 0.045 = 0.00135
0.03 / 0.045 = 0.6666666667

可以看到,未使用 BigDecimal 时是存在明显的误差的,也许称为错误更合适,毕竟在数学上 0.03 * 0.045 是可以得到一个完全确定的结果的,而计算机机计算出的结果显然不让人满意。而在使用了 BigDecimal 类之后,计算结果明显就符合了我们的数学预期。

那么,是否 BigDecimal 就一定能保证计算的精确呢?其实,这个问题要辩证来看。

对于普通的 float 和 double 计算来说,double 的精度最高可达到 16 位,其实看起来也还算不错,但是对于商业计算来说精度就显得不能满足要求了,其数字可能非常大也可能非常小,所以需要一种精度更高表示方法,即 java.math.BigDecimal 类所提供的方法。

要注意的是,最好使用 String 参数来构造 BigDecimal 对象,对于使用 double 类型的参数创建 BigDecimal 对象的弊端 API 文档中的解释如下:

  1. 此构造方法的结果有一定的不可预知性。有人可能认为在 Java 中写入 new BigDecimal(0.1) 所创建的 BigDecimal 正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于 0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 无法准确地表示为double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入 到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
  2. 另一方面,String 构造方法是完全可预知的:写入 new BigDecimal("0.1") 将创建一个 BigDecimal,它正好 等于预期的 0.1。因此,比较而言,通常建议优先使用 String 构造方法
  3. double 必须用作 BigDecimal 的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用 Double.toString(doublele) 方法,然后使用BigDecimal(String)构造方法,将double 转换为String。要获取该结果,请使用static valueOf(double)方法。 
对于这个解释我测试了一下:

import java.math.BigDecimal;

public class BigDecimalCinstructor {
	public static void main(String[] args) {
		BigDecimal unpredictableBD = new BigDecimal(0.1);
		System.out.println("unpredictableBD: " + unpredictableBD);
		
		BigDecimal predicatableBD = new BigDecimal(Double.toString(0.1));
		System.out.println("predicatableBD: " + predicatableBD);

		BigDecimal changedUnpredictableBD = BigDecimal.valueOf(0.1);
		System.out.println("changedUnpredictableBD: " + changedUnpredictableBD);
	}
}

输出结果为:

unpredictableBD: 0.1000000000000000055511151231257827021181583404541015625
predicatableBD: 0.1
changedUnpredictableBD: 0.1

结果证明想要得到精确的结果还是得使用 String 对象创建 BigDecimal 对象,因为,JDK 源码如下所示:

public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        return new BigDecimal(Double.toString(val));
    }

综上所述,想要得到精确的计算结果就必须使用 java.math.Decimal 类,而且官方文档中建议优先使用 String 构造方法。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值