Java Puzzlers 之Puzzle 2: Time for a Change

来源:互联网 发布:recyclerview数据错乱 编辑:程序博客网 时间:2024/05/16 01:52

Puzzle 2: Time for a Change

考虑下面的问题:

Tom去汽车零件店去买价值1.10元的火花塞,但是在他的钱包都是2美元的票子,如果他用两美元的票子买那个火花塞,那么最后能给他找回的零钱是多少呢?

使用用以下的程序来实现,输出的结果是多少呢?

 

public class Change {
    public static void main(String args[]) {
        System.out.println(2.00 - 1.10);
    }

}

 

Solution 2: Time for a Change

很自然的,你可能认为程序输出的结果就是0.9 但是程序怎么知道你想在小数点后面输出两位小数呢?如果你知道关于double转化为字符串的相关规则(在java-API中,有相关文档详细说明了Double.toString)的话,你可以了解到程序打印出足够短的小数去从它最近的相邻的值区别double类型的值,这个数至少一位小数。看起来很合乎情理,那么,程序的输出应该就是0.9 。合乎情理,有时也可能不对。如果你运行此程序,你就会发现结果是0.8999999999999999

问题在于数字1.1 不能正确表示为double,因此它用最接近的double值来表示。在程序中用2减去这个值。不幸的是,结果并不是最接近0.9double值。 最接近结果的值就是在运行上面程序后输出的可怕的数字。

通常情况下,问题在于:并不是所有的数字都可以使用浮点数来完全正确表示。如果你使用5.0以及其后的版本,你就很轻松的使用printf来修正这个错误,能得到完全精确的值:

 

// Poor solution - still uses binary floating-point!
System.out.printf("%.2f%n", 2.00 - 1.10);

 

输出的结果正确,但是并不代表从根本上解决问题:它仍然使用double二进制浮点数算法。浮点数算法提供了很好的广域的值匹配而不是完全的精确结果。二进制浮点计算是很不适合于钱方面的计算。因为它不可能表示0.1或者其他的10的负数幂(特别是有限二进制小数)。

解决方法之一就是使用整数,如int 或者long,使用美分来进行结算。如果使用这样的结算方法,首先必须确保你的整数足够大,以致于能表示这些数。对于上面这个问题,整数完全足够。现在我们使用println来重新结算,使用int值来表示美分值得运算,这个版本中,输出的就是90美分,这个结果就是正确的:

 

System.out.println((200 - 110) + " cents");

 

另外一个解决的办法就是使用BigDecimal,这个可以进行精确的计算。这个也是SQL中的DECEMAL类型经由JDBC转为而成的类型。这里有一个警告:总是使用BigDecimalString)构造函数,而不要使用BigDecimaldouble)。后者将会创建参数的一个“准确”的值:new BigDecimal(.1)将会返回0.1000000000000000055511151231257827021181583404541015625。正确的使用BigDecimal,程序就会输出正确的结果:0.9

 

import java.math.BigDecimal;
 
public class Change {
    public static void main(String args[]) {
        System.out.println(new BigDecimal("2.00").
                           subtract(new BigDecimal("1.10")));
    }

}

 

这个版本也比较简单,就像Java没有提供BigDecimal的语言上的支持一样。使用BigDecimal计算速度也比使用原始的类型计算慢,这导致如果大量的使用小数计算时有可能是个问题。对大部分程序来说并没有影响。

总的来说,在需要很精确的结果实,避免使用float以及double 对于钱方面的计算,使用intlong、或者BigDecimal。对于语言设计者们,需要考虑提供小数算法的很好语言上的支持。一种方法就是提供有限的运算符重载操作,这样就可以使用运算符号来进行数字类型计算,就像BigDecimal那样。另外一种方法就是像COBOL以及PL/L那样提供原始小数类型。

 

 

 

 
原创粉丝点击