这是一个非常核心且重要的概念,因为它涉及到计算机如何存储浮点数,以及由此带来的一些精度问题。

Java 中的 double 类型遵循 IEEE 754 标准,使用 64位(8字节) 来表示一个浮点数,这64位被划分为三个部分:
- 符号位: 1位
0表示正数1表示负数
- 指数位: 11位
用于存储指数部分的值,决定小数点的位置。
- 尾数位: 52位
用于存储有效数字(也称为“ significand” 或 “mantissa”),即数字的精度部分。
各个部分的详细解释
符号位
这是最简单的一部分。

double的值是正数,符号位就是0。double的值是负数,符号位就是1。
示例:
45的符号位是0。-123.45的符号位是1。
指数位
这部分稍微复杂一些,它存储的是一个 偏移码,而不是直接的指数值。
- 偏移量: 对于
double,偏移量是 1023 (即 $2^{11-1} - 1 = 1023$)。 - 计算方式: 计算机存储的指数值 = 实际指数值 + 1023。
- 范围: 实际指数值的范围是 -1022 到 1023。
- 最小的指数值 (存储为
00000000000) 实际上是 -1023,但它被保留用于表示特殊值(如零和非数)。 - 最大的指数值 (存储为
11111111111) 实际上是 1024,它也被保留用于表示无穷大和非数。
- 最小的指数值 (存储为
这种设计可以同时表示正指数和负指数,而无需额外的符号位。
尾数位
尾数位存储的是数字的“有效数字”,为了节省空间,IEEE 754 标准规定,所有浮点数的二进制表示都采用 科学记数法 的形式,并且其整数部分 永远是 1。
- 规范形式:
xxxxx... * 2^E - 隐含的“1”: 因为整数部分永远是1,所以这个“1”在存储时被省略了,被称为“隐含位”或“隐藏位”,这使得尾数位可以多存储一位有效数字,从而提高了精度。
- : 尾数位存储的就是小数点后面的
xxxxx...部分。
示例:
假设一个数的科学记数法是 101 * 2^5,
- 指数
E是5。 - 尾数存储的是
101。
转换步骤:将一个 double 转换为二进制
我们以一个具体的数字为例,625。
第1步:确定符号位
625 是正数,所以符号位为 0。
第2步:将整数和小数部分分别转换为二进制
-
整数部分 (9):
- 9 / 2 = 4 ... 余 1
- 4 / 2 = 2 ... 余 0
- 2 / 2 = 1 ... 余 0
- 1 / 2 = 0 ... 余 1
- 从下往上读,整数部分的二进制是
1001。
-
小数部分 (0.625):
- 625 * 2 = 1.25 ... 取整数部分
1,剩下 0.25 - 25 * 2 = 0.5 ... 取整数部分
0,剩下 0.5 - 5 * 2 = 1.0 ... 取整数部分
1,剩下 0.0 (结束) - 从上往下读,小数部分的二进制是
101。
- 625 * 2 = 1.25 ... 取整数部分
-
组合:
625的完整二进制形式是101。
第3步:转换为科学记数法形式
我们需要将小数点移动到第一个有效数字的后面。
101 = 001101 * 2^3
从 101 到 001101,小数点向左移动了 3 位,所以指数 E 是 3。
- 符号位:
0 - 实际指数:
3 - 尾数:
001101(小数点后面的部分)
第4步:计算指数位和尾数位
-
指数位: 实际指数
3+ 偏移量1023=1026。- 将
1026转换为 11 位二进制:- 1026 / 2 = 513 ... 0
- 513 / 2 = 256 ... 1
- 256 / 2 = 128 ... 0
- 128 / 2 = 64 ... 0
- 64 / 2 = 32 ... 0
- 32 / 2 = 16 ... 0
- 16 / 2 = 8 ... 0
- 8 / 2 = 4 ... 0
- 4 / 2 = 2 ... 0
- 2 / 2 = 1 ... 0
- 1 / 2 = 0 ... 1
- 从下往上读,得到
10000000010。
- 将
-
尾数位: 我们科学记数法中的尾数是
001101,我们需要用0填充到 52 位。- 尾数是
001101后面跟 46 个0。
- 尾数是
第5步:组合成最终的64位二进制
将三部分按顺序拼接起来:
| 符号位 (1位) | 指数位 (11位) | 尾数位 (52位) |
|---|---|---|
0 |
10000000010 |
001101 + 00000000000000000000000000000000000000000000000000 |
最终64位表示:
0 10000000010 0011010000000000000000000000000000000000000000000000
特殊值
IEEE 754 还定义了一些特殊的值,它们通过特定的指数和尾数组合来表示:
- 零:
- 符号位任意 (0 或 1),表示正零或负零。
- 指数位全为
0。 - 尾数位全为
0。
- 非数:
- 指数位全为
1。 - 尾数位不全为
0。 - 通常由无效的运算产生,如
0 / 0.0或Math.sqrt(-1)。
- 指数位全为
- 无穷大:
- 指数位全为
1。 - 尾数位全为
0。 Double.POSITIVE_INFINITY(符号位为0) 和Double.NEGATIVE_INFINITY(符号位为1)。
- 指数位全为
Java 代码示例:查看 double 的二进制表示
你可以使用 Java 的 Double 类来查看一个 double 值的各个部分。
public class DoubleBinaryRepresentation {
public static void main(String[] args) {
double d = 9.625;
printDoubleBinary(d);
System.out.println("\n--- 一个有问题的例子 ---");
double problematic = 0.1;
printDoubleBinary(problematic);
}
public static void printDoubleBinary(double value) {
// 1. 使用 Double.doubleToLongBits 获取64位的 long 表示
long bits = Double.doubleToLongBits(value);
System.out.println("十进制 double 值: " + value);
System.out.println("对应的 long 值: " + bits);
// 2. 分别提取符号位、指数位和尾数位
// 使用位操作
long signBit = (bits >>> 63) & 0x1;
long exponentBits = (bits >>> 52) & 0x7FF; // 0x7FF 是 11位全1的掩码
long mantissaBits = bits & 0x0FFFFFFFFFFFFFL; // 52位全1的掩码
System.out.println("符号位 (1位): " + signBit);
System.out.println("指数位 (11位): " + Long.toBinaryString(exponentBits));
System.out.println("尾数位 (52位): " + Long.toBinaryString(mantissaBits));
// 3. 计算实际的指数值
int actualExponent = (int) exponentBits - 1023;
System.out.println("实际指数值: " + actualExponent);
// 4. 使用 Double 类的方法验证
System.out.println("\n--- 使用 Double 类方法验证 ---");
System.out.println("Double.isNaN(" + value + "): " + Double.isNaN(value));
System.out.println("Double.isInfinite(" + value + "): " + Double.isInfinite(value));
System.out.println("Double.doubleToRawLongBits(" + value + "): " + Double.doubleToRawLongBits(value));
}
}
运行结果分析 (对于 625):
十进制 double 值: 9.625
对应的 long 值: 4638116846983948032
符号位 (1位): 0
指数位 (11位): 10000000010
尾数位 (52位): 11010000000000000000000000000000000000000000000000
实际指数值: 3
--- 使用 Double 类方法验证 ---
Double.isNaN(9.625): false
Double.isInfinite(9.625): false
Double.doubleToRawLongBits(9.625): 4638116846983948032
注意,尾数位的输出和我们手动计算的不完全一样,这是因为 Long.toBinaryString 会省略前导零。001101 在52位中存储时,前面有50个零,总共是52位,代码输出的 .. 实际上是 .. 的有效部分。
为什么 double 存在精度问题?
理解了二进制表示,就能明白为什么 1 + 0.2 不等于 3。
- 十进制:0.1 可以精确表示。
- 二进制:0.1 是一个无限循环小数,它的二进制表示是
000110011001100...。 - 存储限制:
double只有 52 位尾数来存储这个无限循环的小数,它必须进行截断或舍入。
计算机中存储的 1 实际上是一个最接近 1 的、可以用52位尾数表示的二进制浮点数,这个值本身就带有微小的误差,同理,2 和 3 也都是存储的近似值,当这些带有微小误差的数进行运算时,误差会被放大,最终导致结果不精确。
double 的精度问题源于其有限的尾数位无法精确表示某些十进制小数(特别是那些在二进制中是无限循环的数),这是由 IEEE 754 标准决定的,是 double 类型本身的特性,而不是 Java 的 Bug,在需要高精度计算的场景(如金融计算),应使用 java.math.BigDecimal 类。
