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 文档中的解释如下:
- 此构造方法的结果有一定的不可预知性。有人可能认为在 Java 中写入 new BigDecimal(0.1) 所创建的 BigDecimal 正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于 0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 无法准确地表示为double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入 到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
- 另一方面,String 构造方法是完全可预知的:写入 new BigDecimal("0.1") 将创建一个 BigDecimal,它正好 等于预期的 0.1。因此,比较而言,通常建议优先使用 String 构造方法。
- 当 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 构造方法。