一、引言
借用《Effactive Java》这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。但是,货币计算往往要求结果精确,这时候可以使用int、long或BigDecimal。本文主要讲述BigDecimal使用过程中的一些陷阱、建议和技巧。
二、不可变性
BigDecimal是不可变类,每一个操作(加减乘除等)都会返回一个新的对象, 下面以加法操作为例。
BigDecimal a = new BigDecimal( "1.22" );
System.out.println( "construct with a String value: " + a);
BigDecimal b = new BigDecimal( "2.22" );
a.add(b);
System.out.println( "a plus b is : " + a);
我们很容易会认为会输出:
construct with a String value: 1.22
a plus b is :3.44
但实际上a plus b is : 1.22
下面我们就来分析一下加法操作的源码
public BigDecimal add(BigDecimal augend) {
long xs = this .intCompact; //整型数字表示的BigDecimal,例a的intCompact值为122
long ys = augend.intCompact; //同上
BigInteger fst = ( this .intCompact !=INFLATED) ? null : this .intVal; //初始化BigInteger的值,intVal为BigDecimal的一个BigInteger类型的属性
BigInteger snd =(augend.intCompact !=INFLATED) ? null : augend.intVal;
int rscale = this .scale; //小数位数
long sdiff = ( long )rscale - augend.scale; //小数位数之差
if (sdiff != 0 ) { //取小数位数多的为结果的小数位数
if (sdiff < 0 ) {
int raise =checkScale(-sdiff);
rscale =augend.scale;
if (xs ==INFLATED ||(xs = longMultiplyPowerTen(xs,raise)) ==INFLATED)
fst =bigMultiplyPowerTen(raise);
} else {
int raise =augend.checkScale(sdiff);
if (ys ==INFLATED ||(ys =longMultiplyPowerTen(ys,raise)) ==INFLATED)
snd = augend.bigMultiplyPowerTen(raise);
}
}
if (xs !=INFLATED && ys !=INFLATED) {
long sum = xs + ys;
if ( (((sum ^ xs) &(sum ^ ys))) >= 0L) //判断有无溢出
return BigDecimal.valueOf(sum,rscale); //返回使用BigDecimal的静态工厂方法得到的BigDecimal实例
}
if (fst == null )
fst =BigInteger.valueOf(xs); //BigInteger的静态工厂方法
if (snd == null )
snd =BigInteger.valueOf(ys);
BigInteger sum =fst.add(snd);
return (fst.signum == snd.signum) ? new BigDecimal(sum,INFLATED, rscale, 0 ) :
new BigDecimal(sum,compactValFor(sum),rscale, 0 ); //返回通过其他构造方法得到的BigDecimal对象
}
因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b)虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b); 减乘除操作也是一样的返回一个新的BigDecimal对象。
三、构造函数和valueOf方法
首先看如下一段代码:
// use constructor BigDecimal(double)
BigDecimal aDouble = new BigDecimal( 1.22 );
System.out.println( "construct with a double value: " + aDouble);
// use constructor BigDecimal(String)
BigDecimal aString = new BigDecimal( "1.22" );
System.out.println( "construct with a String value: " + aString);
// use constructor BigDecimal.valueOf(double)
BigDecimal aValue = BigDecimal.valueOf( 1.22 );
System.out.println( "use valueOf method: " + aValue);
你认为输出结果会是什么呢?如果你认为第一个会输出1.22,那么恭喜你答错了,输出结果如下:
construct with a double value: 1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22
use valueOf method: 1.22
为什么会这样呢?JavaDoc对于BigDecimal(double)有很详细的说明:
1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在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(double)方法将double转换为String,然后使用BigDecimal(String)构造方法。要获取该结果,使用static valueOf(double)方法。
BigDecimal.valueOf(double) 使用由 Double.toString(double)方法提供的 double的标准化字符串表示形式( canonical string representation) 将 double 转换成 BigDecimal 。这也是比较推荐的一种方式。
BigDecimal.valueOf(double)还有一个重载的方法 BigDecimal.valueOf(long),对于某些常用值(0到10) BigDecimal在内部做了缓存, 如果传递的参数值范围为[0, 10], 这个方法直接返回缓存中相应的BigDecimal对象。
Java源码如下:
/**
* Translates a {@code long} value into a {@code BigDecimal}
* with a scale of zero. This {@literal "static factory method"}
* is provided in preference to a ({@code long}) constructor
* because it allows for reuse of frequently used
* {@code BigDecimal} values.
*
* @param val value of the {@code BigDecimal}.
* @return a {@code BigDecimal} whose value is {@code val}.
*/
public static BigDecimal valueOf( long val) {
if (val >= 0 && val < zeroThroughTen.length)
return zeroThroughTen[( int )val];
else if (val != INFLATED)
return new BigDecimal( null , val, 0 , 0 );
return new BigDecimal(INFLATED_BIGINT, val, 0 , 0 );
}
// Cache of common small BigDecimal values.
private static final BigDecimal zeroThroughTen[] = {
new BigDecimal(BigInteger.ZERO, 0 , 0 , 1 ),
new BigDecimal(BigInteger.ONE, 1 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 2 ), 2 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 3 ), 3 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 4 ), 4 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 5 ), 5 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 6 ), 6 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 7 ), 7 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 8 ), 8 , 0 , 1 ),
new BigDecimal(BigInteger.valueOf( 9 ), 9 , 0 , 1 ),
new BigDecimal(BigInteger.TEN, 10 , 0 , 2 ),
};
附上相应的测试代码:
BigDecimal a1 = BigDecimal.valueOf( 10 );
BigDecimal a2 = BigDecimal.valueOf( 10 );
System.out.println(a1 == a2); // true
BigDecimal a3 = BigDecimal.valueOf( 11 );
BigDecimal a4 = BigDecimal.valueOf( 11 );
System.out.println(a3 == a4); // false
四、equals方法
BigDecimal.equals方法是有问题的.仅当你确定比较的值有着相同的标度时才可使用. 因此,当你校验相等性时注意 - BigDecimal有一个标度,用于相等性比较. 而compareTo方法则会忽略这个标度(scale).
BigDecimal的equals方法源码如下:
@Override
public boolean equals(Object x) {
// 必须是BigDecimal实例
if (!(x instanceof BigDecimal))
return false ;
BigDecimal xDec = (BigDecimal) x;
if (x == this )
return true ;
// 标度必须相同
if (scale != xDec.scale)
return false ;
long s = this .intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor( this .intVal);
return this .inflated().equals(xDec.inflated());
}
参见以下测试代码:
// 打印false
System.out.println( new BigDecimal( "0.0" ).equals( new BigDecimal( "0.00" )));
// 打印false
System.out.println( new BigDecimal( "0.0" ).hashCode() == ( new BigDecimal( "0.00" )).hashCode());
// 打印0
System.out.println( new BigDecimal( "0.0" ).compareTo( new BigDecimal( "0.00" )));
五、对除法使用标度
BigDecimal对象的精度没有限制。如果结果不能终止,divide方法将会抛出ArithmeticException, 如1 / 3 = 0.33333...。所以强烈推荐使用重载方法divide(BigDecimal d, int scale, int roundMode)指定标度和舍入模式来避免以上异常。
参见以下测试代码:
//java.lang.ArithmeticException: Non-terminating decimal expansion;
//no exact representable decimal result.
try {
BigDecimal.valueOf( 1 ).divide(BigDecimal.valueOf( 3 ));
} catch (ArithmeticException ex) {
System.out.println(ex.getMessage());
}
// always use a scale and the rounding mode of your choice
// 0.33
System.out.println(BigDecimal.valueOf( 1 ).divide(BigDecimal.valueOf( 3 ), 2 , BigDecimal.ROUND_HALF_UP));
六、总结
(1)商业计算使用BigDecimal。
(2)使用参数类型为String的构造函数,将double转换成BigDecimal时用BigDecimal.valueOf(double),做除法运算时使用重载的方法divide(BigDecimal d, int scale, int roundMode)。
(3)BigDecimal是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4)尽量使用compareTo方法比较两个BigDecimal对象的大小。
七、参考资料
《Effective Java》
http://www.stichlberger.com/software/java-bigdecimal-gotchas/
http://stackoverflow.com/questions/7186204/bigdecimal-to-use-new-or-valueof
http://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html