Skip to main content

60. 若需要精确答案就应避免使用 float 和 double 类型

  float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果,也不应该在需要精确结果的地方使用。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double。

  例如,假设你口袋里有 1.03 美元,你消费了 42 美分。你还剩下多少钱?下面是一个简单的程序片段,试图回答这个问题:

System.out.println(1.03 - 0.42);

  不幸的是,它输出了 0.6100000000000001。这不是一个特例。假设你口袋里有一美元,你买了 9 台洗衣机,每台 10 美分。你能得到多少零钱?

System.out.println(1.00 - 9 * 0.10);

  根据这个程序片段,可以得到 0.0999999999999999998 美元。

  你可能认为,只需在打印之前将结果四舍五入就可以解决这个问题,但不幸的是,这种方法并不总是有效。例如,假设你口袋里有一美元,你看到一个架子上有一排好吃的糖果,它们的价格仅仅是 10 美分,20 美分,30 美分,以此类推,直到 1 美元。你每买一颗糖,从 10 美分的那颗开始,直到你买不起货架上的下一颗糖。你买了多少糖果,换了多少零钱?这里有一个简单的程序来解决这个问题:

// Broken - uses floating point for monetary calculation!
public static void main(String[] args) {
    double funds = 1.00;
    int itemsBought = 0;
    for (double price = 0.10; funds >= price; price += 0.10) {
        funds -= price;
        itemsBought++;
    }
    System.out.println(itemsBought +"items bought.");
    System.out.println("Change: $" + funds);
}

  如果你运行这个程序,你会发现你可以买得起三块糖,你还有 0.399999999999999999 美元。这是错误的答案!解决这个问题的正确方法是 使用 BigDecimal、int 或 long 进行货币计算。

  这里是前一个程序的一个简单改版,使用 BigDecimal 类型代替 double。注意,使用 BigDecimal 的 String 构造函数而不是它的 double 构造函数。这是为了避免在计算中引入不准确的值 [Bloch05, Puzzle 2]:

public static void main(String[] args) {
    final BigDecimal TEN_CENTS = new BigDecimal(".10");
    int itemsBought = 0;
    BigDecimal funds = new BigDecimal("1.00");
    for (BigDecimal price = TEN_CENTS;funds.compareTo(price) >= 0;price = price.add(TEN_CENTS)) {
        funds = funds.subtract(price);
        itemsBought++;
    }
    System.out.println(itemsBought +"items bought.");
    System.out.println("Money left over: $" + funds);
}

  如果你运行修改后的程序,你会发现你可以买四颗糖,最终剩下 0 美元。这是正确答案。

  然而,使用 BigDecimal 有两个缺点:它与原始算术类型相比很不方便,而且速度要慢得多。如果你只解决一个简单的问题,后一种缺点是无关紧要的,但前者可能会让你烦恼。

  除了使用 BigDecimal,另一种方法是使用 int 或 long,这取决于涉及的数值大小,还要自己处理十进制小数点。在这个例子中,最明显的方法是用美分而不是美元来计算。下面是一个采用这种方法的简单改版:

public static void main(String[] args) {
    int itemsBought = 0;
    int funds = 100;
    for (int price = 10; funds >= price; price += 10) {
        funds -= price;
        itemsBought++;
    }
    System.out.println(itemsBought +"items bought.");
    System.out.println("Cash left over: " + funds + " cents");
}

  总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。