在电商或类电商项目中, 经常会使用货币运算, 货币一般使用double类型, 但是java的double类型有一些奇特的特性. 看下面的代码:
public class DoubleTest {
public static void main(String[] args) {
System.out.println(2.00 - 1.10);
}
}
理想中, 输出的结果应该0.9, 但是现实中残酷的是, 结果看起来非常奇怪:
0.8999999999999999
Process finished with exit code 0
这种情况很常见! 导致我们app在算总价或者其他价格的时候, 界面上偶尔会显示一串小数. 这是为什么呢? 引用Java Puzzlers中的一段话:
如果你对在Double.toString 文档中所设定的将double 类型的值转换为字符串的规则有所了解,你就会知道该程序打印出来的小数,是足以将double 类型的值与最靠近它的临近值区分出来的最短的小数,它在小数点之前和之后都至少有一位。因此,看起来,该程序应该打印0.9 是合理的。这么分析可能显得很合理,但是并不正确。如果你运行该程序,你就会发现它打印的是0.8999999999999999。
问题在于1.1 这个数字不能被精确表示成为一个double,因此它被表示成为最接近它的double 值。该程序从2 中减去的就是这个值。遗憾的是,这个计算的结果并不是最接近0.9 的double 值。表示结果的double 值的最短表示就是你所看到的打印出来的那个可恶的数字。
更一般地说,问题在于并不是所有的小数都可以用二进制浮点数来精确表示的。
当然这段话比较晦涩, 大家能理解就理解, 不能理解就看看解决办法. 一般这种货币运算都不会使用double或者二进制浮点, 通常是使用int或者BigDecimal.
int
为什么要使用int, int不是整型, 不能表示小数点吗? 没错, 要使用int就必须对金额做特殊处理 - 乘以或除以100. 由于货币最多只带两位小数点, 再精确也没有意义, 所以, 在运算之前, 先将运算符两边的货币乘以100, 运算完成后, 再对结果除以100, 然后显示到界面上去. 这样既保证了小数点的位数, 又不会出错. 但是弊端就是每次都要乘以100再除以100, 很麻烦.
BigDecimal
CoreLibs中定义了一个类, MoneyOperation, 专门用于货币计算:
public class MoneyOperation {
private static final int DEF_DIV_SCALE = 10;
private static final String DEF_PATTERN = "0.00";
private MoneyOperation() {}
public static double add(double d1, double d2) {
BigDecimal b1 = new BigDecimal(Double.toString(d1));
BigDecimal b2 = new BigDecimal(Double.toString(d2));
return b1.add(b2).doubleValue();
}
public static double sub(double d1, double d2) {
BigDecimal b1 = new BigDecimal(Double.toString(d1));
BigDecimal b2 = new BigDecimal(Double.toString(d2));
return b1.subtract(b2).doubleValue();
}
public static double mul(double d1, double d2) {
BigDecimal b1 = new BigDecimal(Double.toString(d1));
BigDecimal b2 = new BigDecimal(Double.toString(d2));
return b1.multiply(b2).doubleValue();
}
public static double div(double d1, double d2) {
return div(d1, d2, DEF_DIV_SCALE);
}
public static double div(double d1, double d2, int scale) {
if (scale < 0)
throw new IllegalArgumentException("The scale must be a positive integer or zero");
BigDecimal b1 = new BigDecimal(Double.toString(d1));
BigDecimal b2 = new BigDecimal(Double.toString(d2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static String format(double d) {
return return format(d, DEF_PATTERN);
}
public static String format(double d, String pattern) {
return new DecimalFormat(pattern).format(d);
}
}
如果需要对货币运算, 可以直接使用MoneyOperation提供的加减乘除方法:
public static void main(String[] args) {
System.out.println(MoneyOperation.sub(2, 1.1));
}
输出结果:
0.9
Process finished with exit code 0
也可以调用format方法将double显示成两位小数:
System.out.println(MoneyOperation.format(MoneyOperation.sub(2, 1.1)));
使用MoneyOperation也比较麻烦, 每次都得多谢很多方法调用的代码. 但是为了程序结果准确, 也只能默默忍受了.