AI生成-深入理解 Java 浮点数:从 0.1 + 0.2 ≠ 0.3 说起
深入理解 Java 浮点数:从 0.1 + 0.2 ≠ 0.3 说起
快速了解
1.浮点数的表示

十进制 0.15625 -> 二进制 0.00101 -> 规格化 1.01 × 2⁻³
2.浮点数的误差
浮点数不能精确表示的就会有误差,有误差的数进行运算,最后看误差是不是一样,一样的话就相等,误差不一样就不相等
前言
如果你写过 Java,大概率见过这段代码:
System.out.println(0.1 + 0.2 == 0.3); // false ← 嗯??
System.out.println(0.1 + 0.1 == 0.2); // true ← 这又是为什么?
同样是加法,为什么一个对、一个错?这篇文章将带你从 IEEE 754 标准底层,彻底搞懂浮点数的表示、运算与误差。
一、为什么 0.1 无法被精确存储?
1.1 十进制 → 二进制转换
整数转二进制大家都会,小数怎么办?乘 2 取整法:
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0 ← 回到 0.4,开始循环!
...
0.1₁₀ = 0.00011001100110011...₂ (0011 无限循环)
# 以 0.15625 为例完整演示
0.15625 × 2 = 0.3125 → 0
0.3125 × 2 = 0.625 → 0
0.625 × 2 = 1.25 → 1
0.25 × 2 = 0.5 → 0
0.5 × 2 = 1.0 → 1 (小数部分归零,结束)
0.15625₁₀ = 0.00101₂
0.1 在二进制中是无限循环小数,就像十进制中的 1/3 = 0.333...。计算机的存储空间是有限的,只能截断。
1.2 能精确表示的小数
什么样的十进制小数能被二进制精确表示?
分母只含质因数 2 的小数,二进制表示就是有限的。
0.5 = 1/2 → 0.1₂ ✅ 精确
0.25 = 1/4 → 0.01₂ ✅ 精确
0.125 = 1/8 → 0.001₂ ✅ 精确
0.75 = 3/4 → 0.11₂ ✅ 精确
0.375 = 3/8 → 0.011₂ ✅ 精确
0.1 = 1/10 → 分母有 5 ❌ 无限循环
0.2 = 1/5 → 分母有 5 ❌ 无限循环
0.01 = 1/100 → 分母有 5² ❌ 无限循环
所以 0.5 + 0.25 == 0.75 是 true,这些数在二进制中都是"干净"的。
二、IEEE 754 双精度浮点数的结构
Java 的 double 遵循 IEEE 754 标准,用 64 位存储一个数:
1 位 11 位 52 位
┌────┬─────────────────┬──────────────────────────────────────────┐
│ S │ E │ M │
│符号│ 指数 │ 尾数 │
└────┴─────────────────┴──────────────────────────────────────────┘
63 62 52 51 0
实际值的计算公式:

2.1 三个字段的含义
符号位 S(1 bit):0 正数,1 负数。
指数 E(11 bits,移码):实际指数 = E − 1023,范围 −1022 ~ +1023。
尾数 M(52 bits,原码):存储小数点后的部分。对于规格化数(规格化后都是1.M的形式),隐含前导 1,所以实际精度是 53 位二进制(≈ 15~17 位十进制有效数字)。
# 常用对比
# 十进制表示 = 二进制表示
0.5 = 1/2 = 2的-1次方 = 0.1
0.25 = 0.01
0.125 = 0.001
0.0625 = 0.0001
0.03125 = 0.00001
十进制 0.15625 = 0.125 + 0.03125
│
▼
二进制 0.00101
│
▼
规格化 1.01 × 2⁻³
│
├── S = 0
├── E = -3 + 1023 = 1020 = 01111111100₂
└── M = 01 + 50个0
│
▼
┌─┬───────────┬────────────────────────────────────────────────────┐
│0│01111111100│0100000000000000000000000000000000000000000000000000│
└─┴───────────┴────────────────────────────────────────────────────┘
63 62 52 51 0
这就是 0.15625 在内存中的真实样子
十进制有效位精度
52 位尾数 ≈ 十进制多少位?
2⁻⁵² ≈ 2.22 × 10⁻¹⁶
所以 double 能保证 15~17 位十进制有效数字:
System.out.println(1.0 / 3.0);
// 0.3333333333333333 ← 16 位
// ↑ 第 17 位开始就不准了
2.2 为什么指数用移码?
如果指数用补码,负数指数的二进制编码会大于正数指数:
补码: -1 → 11111111 比 0 (00000000) 还大!
硬件比较两个浮点数大小,最简单的办法是把 64 位当作整数来比。移码让编码顺序 = 数值顺序,从 000... 到 111... 单调递增,硬件整数比较器就能直接比浮点数大小。
2.3 为什么尾数用原码?
- 符号位已经独立:S 位已经拆出去了,尾数不需要再编码符号。
- 对齐右移不能符号扩展:浮点加减时,指数对齐需要把尾数右移,右移补 0。如果用补码,算术右移会符号扩展补 1,破坏数值。
- 乘法简单:原码尾数就是无符号绝对值,直接送乘法器。
三、0.1 和 0.2 的真实存储值
3.1 0.1 的存储
精确 0.1 = 0.00011001100110011...₂(0011 无限循环)
规范化:1.1001100110011001100110011001100110011001100110011010 × 2⁻⁴
↑ 第53位是1,向上舍入
由于尾数只有 52 位,第 53 位必须舍入。0.1 的第 53 位是 1,向上舍入后,实际存储值略大于精确 0.1:
实际值 ≈ 0.1000000000000000055511151231257827021181583404541015625
3.2 0.2 的存储
0.2 是 0.1 的二倍:
0.2 = 1.1001100110011001100110011001100110011001100110011010 × 2⁻³
注意:尾数与 0.1 完全一样,只是指数从 -4 变成了 -3!
实际值 ≈ 0.200000000000000011102230246251565404236316680908203125
3.3 0.3 的存储
0.3₁₀ = 0.01001100110011001100110011...₂(1001 无限循环)
规范化:1.0011001100110011001100110011001100110011001100110011 × 2⁻²
实际值 ≈ 0.299999999999999988897769753748434595763683319091796875
注意:0.3 的实际值略小于精确 0.3!
四、逐表达式精度分析
回到开头的代码:
double a = 0.1 + 0.2;
double b = 0.3;
System.out.println(a == b); // false
double c = 0.1 + 0.1;
double d = 0.2;
System.out.println(c == d); // true
double e = 0.1 * 2;
double f = 0.2;
System.out.println(e == f); // true
double g = 0.3;
double h = 0.3;
System.out.println(g == h); // true
double i = 0.1 * 0.1;
double j = 0.01;
System.out.println(i == j); // false
double l = 0.5 + 0.25;
double m = 0.75;
System.out.println(l == m); // true
# 因为 0.5、0.25、0.75 在二进制中都是精确表示的,所以没有误差
0.5 = 1/2 = 0.1₂ ← 1 位,精确
0.25 = 1/4 = 0.01₂ ← 2 位,精确
0.75 = 3/4 = 0.11₂ ← 2 位,精确
0.125 = 1/8 = 0.001₂ ← 3 位,精确
0.375 = 3/8 = 0.011₂ ← 3 位,精确
0.625 = 5/8 = 0.101₂ ← 3 位,精确
4.1 0.1 + 0.2 == 0.3 → false(经典案例)
0.1: 1.10011001...1010 × 2⁻⁴
0.2: 1.10011001...1010 × 2⁻³
对齐指数(统一为 2⁻³):
0.1 对齐后: 0.110011001...1010 × 2⁻³ ← 低位右移丢失信息
0.2: 1.100110011...1010 × 2⁻³
相加:
0.110011001...1010 0
+ 1.100110011...1010
────────────────────────
10.011001100...0111 0
规范化 → 1.0011001100...01110 × 2⁻²
↑ 第53位是1,后面还有1 → 向上舍入
最终:1.0011001100...0100 × 2⁻²
结果约 0.30000000000000004。
而 0.3 的存储值是约 0.29999999999999999。
双重舍入 vs 单次舍入,产生了不同的结果,相差约 5.55 × 10⁻¹⁷。
4.2 0.1 + 0.1 == 0.2 → true(特殊优化路径)
0.1 + 0.1 等价于 0.1 × 2
在二进制层面,×2 只是指数 +1,尾数完全不变:
1.10011001...1010 × 2⁻⁴
→ 1.10011001...1010 × 2⁻³
而 0.2 的存储值正是 1.10011001...1010 × 2⁻³
尾数和指数完全一致!
x + x(或 x × 2)不涉及任何新的舍入,因为尾数不变。所以结果精确等于 2x 的存储值。
4.3 0.1 * 2 == 0.2 → true
与上同理,×2 = 指数 +1,尾数不动,精确相等。
4.4 0.3 == 0.3 → true
同一个编译期字面量,同一个二进制表示,当然相等。
4.5 0.1 * 0.1 == 0.01 → false
0.1 的近似值平方 ≈ 0.01000000000000000208...
0.01 的近似值 ≈ 0.010000000000000000208...
乘法使两个近似值的误差叠加,与 0.01 的舍入值不同。
五、误差规律总结
| 运算 | 尾数变化 | 是否引入新舍入 | 相等保证 |
|---|---|---|---|
| 同一个字面量 | 无运算 | 否 | ✅ |
x + x / x × 2ⁿ |
尾数不变,只调指数 | 否 | ✅ |
x + y(x ≠ y) |
对齐 + 相加 + 规范化 | 是 | ❌ |
x × y |
尾数相乘 + 舍入 | 是 | ❌ |
x × 2ⁿ(n 为整数) |
尾数不变 | 否 | ✅ |
核心规律:
- 乘以 2 的幂次(
×2, ×4, ×8...):尾数不变,误差同步缩放,一定相等。 - 不同数的加法:需要对齐指数,低位丢失,然后相加再舍入——双重误差来源。
- 分母只有质因数 2 的数:二进制有限,完全精确,不受影响。
- 每次运算最多引入 1 ULP(最低有效位)的舍入误差。
六、正确比较浮点数的方式
6.1 误差范围比较
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-10;
System.out.println(Math.abs(a - b) < epsilon); // true
6.2 使用 BigDecimal(精确计算)
BigDecimal x = new BigDecimal("0.1");
BigDecimal y = new BigDecimal("0.2");
BigDecimal z = new BigDecimal("0.3");
System.out.println(x.add(y).compareTo(z) == 0); // true
⚠️ 注意:必须用字符串构造
new BigDecimal("0.1"),而不是new BigDecimal(0.1)。后者会先把 0.1 转成 double 的近似值,失去了使用 BigDecimal 的意义。
6.3 查看 double 的真实值
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal(0.1 + 0.2));
// 0.3000000000000000444089209850062616169452667236328125
System.out.println(new BigDecimal(0.3));
// 0.299999999999999988897769753748434595763683319091796875
七、特殊值
IEEE 754 还定义了以下特殊值:
| 值 | 符号 S | 指数 E | 尾数 M |
|---|---|---|---|
| +0.0 | 0 | 全 0 | 全 0 |
| -0.0 | 1 | 全 0 | 全 0 |
| +∞ | 0 | 全 1 | 全 0 |
| -∞ | 1 | 全 1 | 全 0 |
| NaN | 任意 | 全 1 | 非 0 |
System.out.println(1.0 / 0.0); // Infinity
System.out.println(-1.0 / 0.0); // -Infinity
System.out.println(0.0 / 0.0); // NaN
System.out.println(Math.sqrt(-1)); // NaN
// NaN 不等于任何值,包括它自己
System.out.println(Double.NaN == Double.NaN); // false
System.out.println(Double.isNaN(Double.NaN)); // true
八、常见面试追问
Q1:float 和 double 有什么区别?
| float | double | |
|---|---|---|
| 位数 | 32 bit | 64 bit |
| 符号 | 1 bit | 1 bit |
| 指数 | 8 bit | 11 bit |
| 尾数 | 23 bit | 52 bit |
| 精度 | 6~7 位十进制 | 15~17 位十进制 |
Q2:float f = 3.14 编译报错,为什么?
3.14 字面量默认是 double 类型,不能隐式转为 float。需要写成 float f = 3.14f。
Q3:为什么循环累加 0.1 十次不等于 1.0?
double sum = 0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
System.out.println(sum == 1.0); // false
每次加法都可能引入舍入误差,10 次累加后误差累积明显。
九、总结
- 浮点数精度问题的根源:二进制无法精确表示分母含 2 以外质因数的十进制小数,52 位尾数截断产生舍入误差。
- 指数用移码:保证编码顺序 = 数值顺序,硬件整数比较器可以直接用。
- 尾数用原码:对齐右移不符号扩展,乘法也简单。
- 乘以 2 的幂次是安全的(尾数不变),不同数的加减乘除则不一定。
- 永远不要用
==直接比较浮点数运算结果,用误差范围或BigDecimal。
浮点数不是魔法,它只是用有限比特近似无限精度的数学现实。理解了 IEEE 754 的 64 位结构,你就不会对
0.1 + 0.2 ≠ 0.3感到困惑了。
浙公网安备 33010602011771号