Skip to content
Go back

BigDecimal——为什么0.1+0.2不等于0.3?

BigDecimal:0.1 + 0.2 为什么不等于 0.3?

一句话结论(30s)

0.1 + 0.2 != 0.3 是因为 IEEE 754 二进制浮点无法精确表示十进制的 0.1,就像十进制无法精确表示 1/3。关键设计是 BigDecimal 用 unscaledValue × 10^(-scale) 的十进制定点模型消除精度误差;代价是软件实现比硬件指令慢几个数量级,且 new BigDecimal(double) 会传入已丢失精度的值,必须用字符串构造或 valueOf

核心原理(2min)

BigDecimal 内部用 intCompact(扩大后的整数)+ scale(小数点位数)+ precision(有效位数)存十进制,计算时对齐 scale 后做整数加减,无二进制截断;除法遇无限循环小数(如 1/3)必须显式指定精度与 RoundingMode,金额计算严禁用 double(0.01 累积误差在亿级流水后可达万元级)。

底层深入(5-10min)

Double 的精度陷阱

System.out.println(0.1 + 0.2);  // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);  // false

根源:IEEE 754 二进制浮点数无法精确表示十进制的 0.1。 就像十进制无法精确表示 1/3 = 0.3333…一样,二进制无法精确表示 1/10。

💭 思考:为什么 0.1 在二进制里是无限循环小数?因为 0.1 = 1/10,分母 10 = 2×5 含因子 5;而二进制只有分母是 2 的幂的数(0.5=1/2、0.25=1/4)才有有限表示。所以 0.5+0.25 精确、0.1+0.2 却多出个尾巴。

BigDecimal 的解决思路

BigDecimal 不使用二进制浮点数——它用十进制定点数模型

BigDecimal = unscaledValue × 10^(-scale)

0.1 → unscaledValue = 1, scale = 1
0.01 → unscaledValue = 1, scale = 2

计算 0.1 + 0.2:
  0.1 = 1 × 10^(-1)
  0.2 = 2 × 10^(-1)
  → 对齐 scale → (1+2) × 10^(-1) = 3 × 10^(-1) = 0.3  ✓ 精确!

内部实现:intCompact(存扩大后的整数)、scale(小数点后位数)、precision(有效位数)。没有二进制截断,完全十进制精确。

💭 思考:BigDecimal 存的是「整数 + scale」,那 0.1+0.2 在它眼里就是 (1+2)×10⁻¹,跟十进制手算一模一样——所以它能精确。可为什么 JDK 不默认所有浮点都用它?因为软件整数运算比硬件浮点指令慢几个数量级,默认全用 BigDecimal 全盘性能会崩。

最经典的坑:构造器传 double

BigDecimal a = new BigDecimal(0.1);  // ❌ 0.1 作为 double 已经有精度丢失!
System.out.println(a);               // 0.1000000000000000055511151231257827021181583404541015625

BigDecimal b = new BigDecimal("0.1"); // ✅ 字符串 → 精确 0.1
BigDecimal c = BigDecimal.valueOf(0.1); // ✅ 内部也是 Double.toString(0.1) = "0.1"

new BigDecimal(double) 接收的是已经丢失精度的 double 值。 字符串构造或 BigDecimal.valueOf(double)(底层走 Double.toString() 规范表示)才是正确做法。

💭 思考new BigDecimal(0.1)new BigDecimal("0.1") 到底差在哪?差在「转换发生的时机」:0.1 字面量在编译期就已是 double(已被截断),构造器只能照单全收;而 "0.1" 是精确的十进制文本,构造器才拿到准确值。valueOf 内部正是先 Double.toString 把它还原成 "0.1" 再解析。

除法与 RoundingMode

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
a.divide(b);  // ❌ ArithmeticException: Non-terminating decimal expansion
a.divide(b, 2, RoundingMode.HALF_UP);  // ✅ 0.33

1/3 是无限循环小数,必须指定精度和舍入模式。这是 BigDecimal 特有的”必设项”——没有默认行为(保护你)。

💭 思考:为什么 divide 遇到 1/3 直接抛异常、不给你默认舍入?因为金额场景里静默舍入会把误差藏起来、越滚越大——宁可用异常逼你显式声明精度和舍入模式,也不悄悄丢精度。这是「保护你」的设计。

BigDecimal vs Double

DoubleBigDecimal
存储IEEE 754 二进制 64bitunscaledValue + scale + precision
0.1 表示近似值精确值
计算速度硬件指令(纳秒级)软件实现(微秒级)
适用场景科学计算、图形渲染金融计算(金额/利率)

金额永远用 BigDecimal,决不要用 double。 0.01 元的累积误差在亿级流水后就是万元级别的差错了。

章末提问

追问 1:0.1+0.2 为什么不等于 0.3,但 0.5+0.25 却精确等于 0.75?

结论先行:因为 0.5、0.25 是 2 的负整数次幂,二进制能有限精确表示;而 0.1、0.2 转二进制是无限循环小数,只能被截断成近似值。

因为:IEEE 754 用二进制分数表示小数,只有分母是 2 的幂的数(1/2、1/4、1/8…)才有有限二进制表示;0.1=1/10 的分母含因子 5,转二进制必然无限循环,尾数被截断后相加再转回十进制就出现了 0.30000000000000004

追问 2:new BigDecimal(0.1)new BigDecimal("0.1") 有什么区别?valueOf 内部做了什么?

结论先行:前者拿到的是已被截断的 double 值(会得到 0.1000…0555 的长串),后者是精确的 0.1;valueOf 内部用 Double.toString(0.1) 把 double 还原成 “0.1” 再走字符串构造。

因为0.1 字面量在编译期就已按 IEEE 754 存储、精度已丢失,new BigDecimal(double) 只能忠实记录这个「已经错掉」的值;而字符串构造直接解析十进制文本,不存在二进制中转。valueOf 之所以精确,是因为 Double.toString 输出的是能唯一区分该 double 的最短十进制表示。

追问 3:金额计算为什么必须用 BigDecimal,它比 double 慢多少、慢在哪?

结论先行:BigDecimal 用软件整数运算保证十进制精确,代价是比硬件浮点指令慢几个数量级(纳秒级 → 微秒级)。

因为:double 的加减乘除直接映射到 CPU 硬件指令;BigDecimal 需要对齐 scale、用 intCompact/BigInteger 做软件整数运算并维护 precision,每一步都是多条指令。金额对精度是刚需(0.01 的累积误差在亿级流水后就是万元级差错),性能换正确性是值得的。


Share this post on:

Previous Post
ByteBuffer——HeapByteBuffer与DirectByteBuffer的本质区别
Next Post
BIO的ServerSocket.accept()——从Java到内核的完整调用链