Java 中基本类型中,表示小数的有 float 和 double。与他们相关的几个问题会经常出现,并且困扰着我们,例如:float 和 double 为什么叫做浮点数?他们的格式是什么样的?如何把一个十进制小数转化为二进制?为什么会出现 NaN 这中奇怪的东西?
在接下的的章节中,我们将逐一讨论这些问题。
1. 浮点数
准确的说,“浮点数”是一种表示数值的方法,类似的还有“定点数”。定点数是指数值中的小数点位置固定的数值表示方式。例如 3.141 和 2.500 是小数点在三位小数之前的定点数。而浮点数就是相对定点数来说,数值中的小数点位置不固定的数值表示方式。例如,2.500 就可以表示为 0.25* 10^1 或 25 * 10^-1。
可以看出,定点数在数值的表示上非常的“死板”,小数点的位置决定了小数部份的位数,并且在小数位数不足的情况下,还需要使用 0 来补全。浮点数则不存在这样的问题。
2. 十进制转二进制
十进制数转二进制通常是将整数部分和小数部份分开处理,先将整数部份转化为二进制,然后将小数部份转化为二进制,最后将整数部份和小数部份的结果和并起来。
对于整数部份来说,十进制转二进制通常使用除 2 取余数的方式来计算。如图 1 。
图 1图 1 展示了十进制整数 26 转化为二进制整数 11010 的过程。十进制整数转二进制整数的基本过程是将十进制整数除以 2 ,记录余数后将商再除以 2 记录余数,如此反复,直至商小于 2 为止,将这个得到的最后的商也记录,最后把所有记录下的数翻转,即得到与十进制对应的二进制形式。图 1 中的具体过程如下:
1. 将 26 除以 2,商 13 余数为 0,记 0,累计为 0;
2. 将上一步的商做为被除数 13 除以 2,商 6 余数为 1,记 1,累计为 01;
3. 将上一步的商做为被除数 6 除以 2,商 3 余数为 0,记 0,累计为 010;
4. 将上一步的商做为被除数 3 除以 2,商 1 余数为 1,记 1,累计为 0101;
5. 上一步的商 1 ,小于 2 ,不再做除法并记录商,记 1,累计为 01011;
6. 将所有记录的数字翻转,得 11010,即为 26 的二进制形式。
对于小数部份来说,十进制转二进制通常采用乘 2 取整数的方式来计算。如图 2 。
图 2图 2 展示了十进制小数 0.8125 转化为二进制小数 0.1101 的过程。十进制小数转二进制小数的基本过程是将十进制小数乘以 2,记录整数部分后,将积减去其整数部分后再乘以 2 记录整数部分,如此反复,直至积等于 0 或到达指定精度 [1]。图 2 中具体过程如下:
1. 将 0.8125 乘以 2,积为 1.625,取整数部分记 1,累计 1;
2. 将上一步的积减去整数部分后得 0.625 ,乘以 2 ,积为 1.25,取整数部分记 1,累计 11;
3. 将上一步的积减去整数部分后得 0.25 ,乘以 2 ,积为 0.5,取整数部分记 0,累计 110;
4. 将上一步的积减去整数部分后得 0.5 ,乘以 2 ,积为 1,取整数部分记 1,累计 1101;
5. 积为 1 不再做乘法,记录加小数点为 0.1101,即为 0.8125 的二进制形式。
经过以上计算,分别计算出了 26 和 0.8125 的二进制形式,如果我们将其合并起来,就得到了 26.8125 的二进制形式 11010.1101。
3. 浮点数的格式
明白如何从数的十进制形式得到的二进制形式后,需要理解在 Java 中是如何存储浮点数的。Java 遵循 IEEE 754 标准,它将一个浮点数分为“符号”、“阶码”和“尾数”3 个部分,并且定义了浮点数的表达方式,如图 3。
图 3图 3 中 d.dd...d 即尾数,β 为基数,e 为指数。二进制中的 β 必然为 2,因此 d 的值必然只能为 0 或 1,并且定义了小数点前的第一个 d 必须是 1 。接下来图 4 展示了双精度浮点数 [2] 的格式。
图 4先说明图 3 与图 4 间的关联。根据浮点数的定义,图 4 中的“符号”与图 3 中的 ± 号相对应,用于表示浮点数是正数还是负数,正数为 0 负数为 1;图 4 中的“尾数”与图 3 中 d.dd...d 相对应,用于表示浮点数中的有效数值,根据定义,图 4 中的尾数将忽略图 3 中的小数点以及小数点前的一个 d,默认其总是为 1;图 4 中的“阶码”与图 3 中的 e 对应,根据定义,双精度浮点数的阶码 = e + 1023;最后图 4 中没有任何与图 3 中的 β 所对应的部分,而是在二进制中总是默认为 2 。
大致了解了浮点数的结构,接下来通过实践来验证一下。
在前面的章节中,有得出十进制的 26.8125 的二进制形式为 11010.1101。根据图 3 中给出的定义,必须满足小数点前有一位的形式,因此调整 11010.1101 ,使其变为 1.10101101 * 2^4 。至此,推导出“符号”、“阶码”和“尾数”3 个部分:
符号为 0。因为根据定义,正数为 0,负数为 1。
阶码为 10000000011。因为 e = 4,根据定义,阶码 = e + 1023,阶码为 1027 的二进制形式。
尾数为 10101101。因为 1.10101101 中需要忽略小数点以及前一个 d。
接下来,将计算所得的几个数值根据双精度浮点数的格式进行处理:阶码已经有 11 位,不需要补全;尾数只有 8 位,而图 4 中标明了位数需要 52 位,使用 44 个 0 补全。然后,编写一段代码,验证一下手动计算得到的结果是否可以还原为原始的 26.8125。[3]
图 54. 特殊值
浮点数定义中指定了几个特殊值用于表示 0、 NaN 和 Infinity :
0 为阶码全为 0 且位数全为 0;
NaN 为阶码全为 1 且尾数不全为 0;
Infinity 为阶码全为 1 且尾数全为 0。
根据定义,浮点数应该存在 +Infinity、-Infinity、+NaN、-NaN、 +0、-0、等几种形式。可以使用如图 5 的方式一一验证所有的正特殊值。但由于图 5 中的代码在处理的中间使用了 long 作为过渡,因此 long 值如果为负数时,将会被表示为补码。根据源码补码转源码的定义(源码 = 补码 - 1 后除符号位取反),以 -Infinity 为例,则 -Infinity 符号为 1,阶码全为 1,尾数全为 0。如果使用原码表示,则为:
1111111111110000000000000000000000000000000000000000000000000000
如果此值为补码,转为源码后的表示则为:
1000000000010000000000000000000000000000000000000000000000000000
下面同样使用如图 5 的方法进行验证。
图 6 图 7图 6 和图 7 分别验证了 +Infinity 和 -Infinity。由于 long 的补码原因,这个方式无法验证 -NaN 和 -0。[4]
[1] 例如双精度浮点数只有 52 位尾数用于表示这个运算的结果,因此这个运算是不可能无限继续下去。
[2] Java 中的双精度浮点数为 double。与双精度浮点数类似的,还有单精度浮点数、扩展精度浮点数等等。
[3] 此方法首先将指定的、表示二进制数值的字符串转化为一个 long 值,然后调用方法 Double.longBitsToDouble(long) 将这个 long 值转化为一个拥有相同二进制表示的 double 值。
[4] 这并不在这篇文章的讨论范围内,你可以运行代码 System.out.println(-Double.NaN== Double.NaN);System.out.println(Double.compare(-Double.NaN, Double.NaN));System.out.println(-0.0d== 0.0d);System.out.println(Double.compare(-0.0d, 0.0d)); 比较并思考运行结果,或查阅其他资料。
网友评论