借用《Effactive Java》这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供彻底精确的结果,因此不该该被用于要求精确结果的场合。可是,货币计算每每要求结果精确,这时候可使用int、long或BigDecimal。本文主要讲述BigDecimal使用过程当中的一些陷阱、建议和技巧。 html
BigDecimal是不可变类,每个操做(加减乘除等)都会返回一个新的对象, 下面以加法操做为例。 java
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 ide
a plus b is :3.44 函数
但实际上a plus b is : 1.22 测试
下面咱们就来分析一下加法操做的源码 this
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对象。 spa
首先看以下一段代码: 设计
// 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,那么恭喜你答错了,输出结果以下: code
construct with a double value: 1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22
use valueOf method: 1.22
为何会这样呢?JavaDoc对于BigDecimal(double)有很详细的说明:
一、参数类型为double的构造方法的结果有必定的不可预知性。有人可能认为在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(double)方法将double转换为String,而后使用BigDecimal(String)构造方法。要获取该结果,使用static valueOf(double)方法。
/** * 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
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")));
参见如下测试代码:
//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