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.9的double值。 最接近结果的值就是在运行上面程序后输出的可怕的数字。
通常情况下,问题在于:并不是所有的数字都可以使用浮点数来完全正确表示。如果你使用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转为而成的类型。这里有一个警告:总是使用BigDecimal(String)构造函数,而不要使用BigDecimal(double)。后者将会创建参数的一个“准确”的值: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; 对于钱方面的计算,使用int、long、或者BigDecimal。对于语言设计者们,需要考虑提供小数算法的很好语言上的支持。一种方法就是提供有限的运算符重载操作,这样就可以使用运算符号来进行数字类型计算,就像BigDecimal那样。另外一种方法就是像COBOL以及PL/L那样提供原始小数类型。