答案
在处理金融计算和表示金额时,是不能使用浮点数的(如 float
和 double
),主要原因是涉及到浮点数的精度问题。
在计算机中所有的数据都要转换为二进制才能被计算机处理,但很遗憾,很多十进制小数在转换为二进制表示时是无限循环小数,这就导致了精确度的损失。所以,浮点数只是近似值,并不是精确值,不能用来表示金额,因为会有精度丢失。
扩展
浮点精度丢失
先看如下程序:
@Test
public void test01() {
float f1 = 6.6f;
float f2 = 1.3f;
System.out.println(f1 + f2);
}
// 执行结果
7.8999996
得到的答案并不是我们预想中的 0.9。再看一个:
@Test
public void test02() {
float sum = 0;
for (int i = 0 ; i < 10 ; i++) {
sum += 0.1;
System.out.println(sum);
}
}
执行结果:
0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001
1.0000001
结果也与我们预想中的不一样,为什么会出现这种情况呢?主要原因有如下几个:
- 基于二进制的表示法
- 有限的表示空间
- 舍入误差的积累
基于二进制的表示法
计算机内部使用二进制来存储和处理所有数据,包括浮点数,而我们常用的十进制数必须转换为二进制形式才能被计算机处理。一个十进制数转换二进制数分为整数部分和小数部分,最后通过拼接的方式来表示一个完整的十进制数。
十进制整数转换为二进制
对于正整数和 0 ,直接转换为二进制形式。而对于负整数,使用补码进行表示。
正整数转换为二进制步骤如下:
- 将整数除以 2。
- 记录余数。
- 将商作为新的数值重复步骤 1 和 2,直到商为 0。
- 将所有余数倒序排列,即为该整数的二进制表示。
以 6 为例
除以 2 | 商 | 余数 |
---|---|---|
6 / 2 |
3 | 0 |
3 / 2 |
1 | 1 |
1 / 2 |
0 | 1 |
所以,6 的二进制表示为 0110
。按照同样的方式 3 的二进制表示为 0011
负整数转换为二进制步骤如下:
- 先将对应的正整数转换为二进制。
- 然后将所有位取反(0变为1,1变为0)。
- 最后,给结果加1。
比如计算 -6
的二进制表示:
- 6 的二进制表示为
0110
。 - 取反:
1001
。 - +1,则为
1010
。
十进制小数转换为二进制
整数采取除以 2 的模式来转换为二进制,小数部分则是通过乘以2的方法转换:
- 将小数部分乘以2。
- 记录结果的整数部分(0或1)作为二进制位。
- 取结果的小数部分作为新的数值重复步骤1和2,直到结果为0或达到所需的精度。
- 将步骤2中记录的所有二进制位按顺序排列,即为小数部分的二进制表示。
我们用 0.6 来演示下:
乘以 2 | 整数部分 | 小数部分 |
---|---|---|
0.6 * 2 |
1 | 0.2 |
0.2 * 2 |
0 | 0.4 |
0.4 * 2 |
0 | 0.8 |
0.8 * 2 |
1 | 0.6 |
0.6 * 2 |
1 | 0.2 |
...... |
进入循环,所以 0.6 的二进制表示为 1001100110...
。按照同样的方式,可以得到 0.3 的二进制表示为0100110011...
。
所以这里 6.6 和 3.3 的二进制表示如下:
- 6.6:
110.1001100110...
- 3.3:
11.0100110011...
这两个无效循环小数相加肯定不能得到一个精确的小数值。
有限的表示空间
为了解决小数转换为二进制无限循环而导致无法精确表示的情况,于是就有了 IEEE 754
标准。IEEE 754
规定了浮点数的存储方式、算术格式等方法。
在 IEEE 754
标准中,一个浮点数有 3 部分组成:符号(sign)、指数(exponent)、尾数(或称为有效数,mantissa),具体表现形式如下:
- 符号(sign):1位,0表示正数,1表示负数
- 指数(exponent):对于单精度浮点数,使用8位;对于双精度,使用11位。指数字段存储的值是实际指数加上一个偏移量(bias)。例如,在单精度浮点数中,偏移量是127,所以指数值123实际上表示
2^-4
。 - 尾数(mantissa):在单精度中,23位;在双精度中,52位。尾数部分代表了数值的精确部分,它前面隐含一个1(对于非零数值),这意味着实际的尾数是1.xxxx形式,其中xxxx是存储在尾数字段中的值
由于尾数部分的位数是有限的,这意味着只能表示有限数量的小数位。因此,即使某些十进制小数可以精确转换为二进制形式,如果这些小数的二进制表示超出了尾数部分可以表示的位数,就必须对其进行四舍五入,这就会导致精度丢失。