Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题

Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题

Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题

当你在 Java 中写出:

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

是不是很诧异?这不是 Java 的“bug”,而是计算机数值表示方式导致的。本文系统解释 为什么 double(IEEE 754 二进制浮点)会出现精度问题,以及 BigDecimal 为什么能精确表示十进制小数。

一、问题概览:现象与根源

现象:

使用 double 进行十进制计算时,常会看到奇怪的结果:

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

原因:

double 使用 IEEE 754 二进制浮点标准(基数 2)。

很多十进制有限小数(如 0.1)在二进制下是无限循环小数,计算机只能存有限位数 → 截断 → 误差。

后果:

在金融、计费、利率、货币运算等场景中,这种微小误差可能引起严重问题。

因此此类场景应使用 BigDecimal(十进制精确数)来代替 double。

二、二进制与十进制表示小数的区别

任意进制的小数都可以写作:

[

0.d_1d_2d_3..._b = d_1·b^{-1} + d_2·b^{-2} + d_3·b^{-3} + ...

]

例如:

十进制:0.1 = 1×10⁻¹

二进制:0.1₂ = 1×2⁻¹ = 0.5₁₀

只有分母是底数的幂时,小数才能有限表示:

例子

分母

十进制能否有限

二进制能否有限

1/2

2

1/5

5

1/10 (=0.1)

2×5

1/8 (=0.125)

8

也就是说:

十进制能精确表示分母含 2、5 的分数;

二进制只能精确表示分母是 2ⁿ 的分数。

因此,0.1(=1/10)在二进制中是无限循环小数。

三、0.1 的二进制展开(为什么无限循环)

转换规则:

每次“乘以 2,取整数部分作为当前二进制位,小数部分继续乘 2”。

以 0.1 为例:

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)

0.6 × 2 = 1.2 → 1 (余 0.2)

余数又回到 0.2 → 循环出现!

因此:

0.1₁₀ = 0.0001100110011...₂

即 0.(0011)₂ 无限循环。计算机只能存有限位(double 仅 53 位有效位),因此必须截断或舍入。

四、IEEE 754 double 的结构与近似误差

double 占 64 位:

部分

位数

说明

符号位

1

表示正/负

指数

11

偏移量 1023

尾数(mantissa)

52

实际有效精度约 15~17 位十进制数

double 在存储 0.1 时,会存成一个最接近 0.1 的二进制浮点数:

0.1000000000000000055511151231257827021181583404541015625

当你做 0.1 + 0.2 时,其实是两个近似数相加,误差传播后就看到:

0.30000000000000004

五、BigDecimal:为什么它不会有这个问题

BigDecimal 是十进制数的精确实现。

它不是浮点数,而是:

即:

unscaledValue:一个整数(BigInteger)

scale:小数位数(十进制)

例如:

BigDecimal x = new BigDecimal("0.003");

System.out.println(x.unscaledValue()); // 3

System.out.println(x.scale()); // 3

// 表示 3 ÷ 10³ = 0.003

再如:

BigDecimal y = new BigDecimal("0.234");

System.out.println(y.unscaledValue()); // 234

System.out.println(y.scale()); // 3

// 表示 234 ÷ 10³ = 0.234

这意味着 BigDecimal 直接存储十进制结构,没有任何二进制浮点参与,因此不会出现“无限循环导致误差”。

六、构造 BigDecimal 的正确方式

✅ 推荐写法:

BigDecimal bd1 = new BigDecimal("0.1"); // 字符串精确

BigDecimal bd2 = BigDecimal.valueOf(0.1); // 内部等价于 new BigDecimal("0.1")

❌ 错误写法:

BigDecimal bd3 = new BigDecimal(0.1); // 把 double 的误差一并带入

System.out.println(bd3);

// 0.1000000000000000055511151231257827021181583404541015625

七、两者比较的本质区别

特性

double

BigDecimal

基数

2(二进制)

10(十进制)

能精确表示的数

n/2ⁿ

n/10ⁿ

存储结构

符号 + 指数 + 尾数

BigInteger + scale

精度

固定(约15位十进制)

任意(受内存限制)

性能

快(硬件支持)

慢(软件实现)

误差来源

二进制无法精确表示十进制

无(除非除法需舍入)

八、实践建议

使用 double 的场景:

科学计算、统计分析、图形渲染等对微小误差不敏感、追求性能的场景。

使用 BigDecimal 的场景:

金融、税务、计费等对精度要求极高的场景。

需要精确比较或精确小数运算时。

建议:

使用 new BigDecimal(String) 或 BigDecimal.valueOf(double)

明确指定 RoundingMode(舍入方式)和 scale(小数位数)

九、示例代码(对比)

public class PrecisionDemo {

public static void main(String[] args) {

double a = 0.1;

double b = 0.2;

System.out.println("double 0.1 + 0.2 = " + (a + b));

// 0.30000000000000004

BigDecimal bd1 = new BigDecimal("0.1");

BigDecimal bd2 = new BigDecimal("0.2");

System.out.println("BigDecimal(\"0.1\") + BigDecimal(\"0.2\") = " + bd1.add(bd2));

// 0.3

BigDecimal fromDouble = new BigDecimal(0.1);

System.out.println("new BigDecimal(0.1) = " + fromDouble);

// 0.1000000000000000055511151231257827021181583404541015625

}

}

十、小结(重点回顾)

double 是 二进制浮点数,只能精确表示分母为 2ⁿ 的分数。

十进制小数(如 0.1)在二进制中变成 无限循环,只能近似。

BigDecimal 用十进制结构(unscaledValue / 10^scale)存数,能精确表示所有十进制有限小数。

构造时应使用 字符串或 valueOf,不要直接传入 double。

十进制并不是万能精确——像 1/3 在十进制中也会无限循环,但 BigDecimal 可以用设定精度来控制结果。

🔍 一句话总结:

二进制浮点数 double 对十进制小数“天生不友好”;

而 BigDecimal 以十进制整数 + 小数位数的方式存储,天生适合精确的十进制运算。

相关文章

揭秘电子保单:保险合同的新形式,你了解多少?
28365-365体育备用

揭秘电子保单:保险合同的新形式,你了解多少?

⌛ 06-18 👁️ 252
如何从零开始做一名新媒体运营?
英国投注网站365

如何从零开始做一名新媒体运营?

⌛ 10-15 👁️ 694