问题描述
在《Java解惑》谜题2 中,作者提出 2.00 – 1.10 的浮点数计算,提到浮点数计算的不精确问题。
以下程序:
1: public class Test {
2: public static void main(String[] args) {
3: System.out.println(2.00-1.10);
4: }
5: }
我们期望打印出浮点数的差值,即0.9,但很不幸,结果打印的是0.8999999999999999,问题就在于并不是所有的小数都是可以用二进制浮点数来精确表示的。
分析问题
浮点数,即小数点的位置可以浮动的数,如
352.47 = 3.5247 * 102
= 3524.7 * 10-1
= 0.35247 * 103
显然,这里小数点的位置是变化的,但因为分别乘上了不同的10 的方幂,故值不变。
通常,浮点数被表示成
式中,S为尾数(可正可负),j为阶码(可正可负),r是基数(或基值)。在计算机中,基数可取2、4、8或16等。
以基数 r=2 为例,数N 可写成下列不同的形式:
= 0.110101 * 210
= 1.10101 * 21
= 1101.01 * 2-10
= 0.00110101 * 2100
.
.
.
为了提高数据精度以及便于浮点数的比较,在计算机中规定浮点数的尾数用纯小数形式,故上例中0.110101 * 210 和 0.00110101 * 2100 形式是可以采用的。此外,将尾数最高位为1 的浮点数成为规格化数(尾数部分的最高位和符号位相反),即 0.110101 * 210 为浮点数的规格化形式。浮点数表示成规格化形式后,其精度最高。
当十进制数转换成计算机中二进制的时候,分为整数和小数两部分,转换方法为:
整数-------除2 倒取余
小数-------乘2 正取余
例如十进制整数 10转换成二进制:
10 除2 将余数倒着排列既是 1010, 二进制的 10.
十进制小数 0.125 的二进制表示为:
将十进制小数乘2 后取其整数部分,注意,如果乘2 后的得数大于 1, 则余数为1 且下一次乘2 的因数要减1 。即
现在再来看《Java解惑》中提到的 2.0 – 1.10 问题,2.0 的换成二进制即为 10.0,没有需要截断或丢失精度的问题。而 1.10 则不同了,整数部分为 1,小数部分,即0.10 则无法用二进制来精确表示,按照 “乘2 正取余” 的方法,我们来看:
计算到这里,我们就会发现,计算是无限循环下去没有终止的,就意味着 0.10 这个小数是无法用二进制来精确的表示的,所以,它被表示成最接近真实值的一个近似值,所以在得出结果的时候,我们没有看到期望的 0.9, 取而代之的是0.8999999999999999。
在文章的结尾,作者指出,二进制浮点对于货币计算是非常不适合的,因为它不可能将0.1,或者10 的其他任何次负数幂精确的表示为一个长度有限的二进制小数,可见上边0.10 转换的例子。
解决方法
总之,在需要精确答案的地方,要避免使用 float 和 double,对于货币计算,要使用 int、long 或 BigDecimal。下面给出用 BigDecimal 来解决问题的代码:
1: import java.math.BigDecimal;
2:
3: public class Test {
4: public static void main(String[] args) {
5: System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10")));
6: }
7: }
8:
注意:一定要用 BigDecimal(String)构造器,而千万不要用 BigDecimal(double)。后一个构造器将用它的参数的“精确”值来创建一个实例:
new BigDecimal(.1)将返回一个表示0.100000000000000055511151231257827021181583404541015625 的 BigDecimal。通过正确使用BigDecimal,程序就可以打印出我们所期望的结果 0.90。