货币运算

在电商或类电商项目中, 经常会使用货币运算, 货币一般使用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也比较麻烦, 每次都得多谢很多方法调用的代码. 但是为了程序结果准确, 也只能默默忍受了.

Last updated