BigDecimal 与精确小数运算 | JavaSE
BigDecimal 与精确小数运算
一、学习目标
完成本章后,你应该能够:
- 解释为什么
double、float在某些十进制小数运算中会出现精度误差。 - 理解
BigDecimal的核心定位:任意精度的十进制数值运算。 - 正确使用
new BigDecimal(String)和BigDecimal.valueOf(double)创建 BigDecimal。 - 明确为什么通常不推荐直接使用
new BigDecimal(0.1)。 - 熟练使用
add()、subtract()、multiply()、divide()完成四则运算。 - 理解除法中的
scale与RoundingMode。 - 熟练使用
setScale()处理金额保留小数位数。 - 准确区分
equals()与compareTo()的比较语义。 - 理解 BigDecimal 的不可变性。
- 能够使用 BigDecimal 完成商品价格、折扣、税费等实际金额计算。
二、核心知识
2.1 为什么 0.1 + 0.2 不一定等于我们看到的 0.3
先观察:
public class DoubleDemo {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
System.out.println(a + b);
}
}
你可能认为输出:
0.3
实际常见结果是:
0.30000000000000004
这并不是 Java 把小学数学算错了。
真正的问题在于:
计算机中的
float和double使用二进制浮点表示,而很多有限十进制小数无法用有限长度的二进制小数精确表示。
2.2 十进制无法有限表示与二进制无法有限表示
十进制中:
1 / 3
=
0.333333333...
无法使用有限位十进制小数精确表示。
类似地,在二进制中:
0.1
也不能用有限长度的二进制小数精确表示。
因此计算机保存的实际上只能是:
非常接近 0.1 的某个二进制浮点数
于是:
近似值
+
近似值
=
另一个近似值
最终就可能出现:
0.30000000000000004
2.3 double 是不是不能用
不是。
这是学习 BigDecimal 时非常容易走向的另一个极端。
double 非常适合:
- 科学计算
- 图形计算
- 物理模拟
- 统计计算
- 对微小浮点误差能够容忍的计算
例如:
double temperature = 36.5;
double height = 1.78;
double distance = 125.67;
完全正常。
真正需要格外谨慎的是:
业务规则要求十进制结果精确、舍入规则明确的场景。
例如:
商品金额
账单金额
利息
税费
折扣
财务计算
这就是 BigDecimal 最典型的应用领域。
2.4 BigDecimal 是什么
BigDecimal 位于:
java.math
包中。
需要导包:
import java.math.BigDecimal;
它可以理解为:
Java 提供的任意精度十进制数值类型。
例如:
BigDecimal price =
new BigDecimal("19.99");
此时我们能够精确表达:
19.99
而不需要依赖二进制浮点数对十进制小数进行近似。
2.5 BigDecimal 的核心结构:数值 + scale
BigDecimal 可以从概念上理解为:
unscaled value × 10^(-scale)
例如:
19.99
可以理解成:
unscaled value = 1999
scale = 2
所以:
1999 × 10^-2
=
19.99
再例如:
2.00
可以理解成:
unscaled value = 200
scale = 2
这解释了后面一个非常重要的现象:
new BigDecimal("2.0")
与:
new BigDecimal("2.00")
虽然数值大小一样,但是内部表示中的:
scale
并不一样。
2.6 BigDecimal 是不可变对象
BigDecimal 与前面学习过的:
String
LocalDate
LocalDateTime
一样,也属于不可变对象。
例如:
BigDecimal price =
new BigDecimal("100");
price.add(
new BigDecimal("20")
);
System.out.println(price);
输出仍然是:
100
因为:
price.add(...)
并没有修改 price 本身。
而是:
根据原 BigDecimal 计算并返回一个新的 BigDecimal。
因此应该:
BigDecimal result =
price.add(
new BigDecimal("20")
);
或者:
price =
price.add(
new BigDecimal("20")
);
三、使用方法
3.1 创建 BigDecimal:字符串构造器
最直观的方式:
BigDecimal price =
new BigDecimal("19.99");
注意参数:
"19.99"
是一个:
String
而不是:
double
这样 BigDecimal 可以直接按照字符串中的十进制数字建立精确的十进制数值。
例如:
BigDecimal a =
new BigDecimal("0.1");
BigDecimal b =
new BigDecimal("0.2");
BigDecimal result =
a.add(b);
System.out.println(result);
输出:
0.3
3.2 不推荐这样创建小数 BigDecimal
下面这行代码虽然能编译:
BigDecimal number =
new BigDecimal(0.1);
但它通常不是我们想要的效果。
原因:
0.1
在传入构造器之前,就已经先成为了一个:
double
而这个 double 并不能精确表示十进制 0.1。
因此:
new BigDecimal(0.1)
得到的是:
这个 double 二进制浮点值对应的精确十进制表示。
而不是你肉眼理解的“数学上的精确 0.1”。
3.3 对比实验
import java.math.BigDecimal;
public class BigDecimalCreateDemo {
public static void main(String[] args) {
BigDecimal a =
new BigDecimal(0.1);
BigDecimal b =
new BigDecimal("0.1");
System.out.println(a);
System.out.println(b);
}
}
第二个结果就是我们真正希望表示的:
0.1
而第一个会带有大量额外十进制位。
所以请建立非常重要的习惯:
明确写死的十进制值
→ new BigDecimal("0.1")
而不是:
new BigDecimal(0.1)
3.4 BigDecimal.valueOf()
如果手中已经是:
double
通常可以:
BigDecimal.valueOf(doubleValue)
例如:
double price = 0.1;
BigDecimal value =
BigDecimal.valueOf(price);
valueOf(double) 会基于该 double 的规范字符串表示进行转换。
例如:
BigDecimal a =
BigDecimal.valueOf(0.1);
BigDecimal b =
BigDecimal.valueOf(0.2);
System.out.println(
a.add(b)
);
得到:
0.3
3.5 到底使用 String 构造还是 valueOf
可以建立下面的实用规则。
程序中明确写一个精确十进制值
优先:
new BigDecimal("19.99")
例如:
BigDecimal taxRate =
new BigDecimal("0.13");
数据已经是 double
使用:
BigDecimal.valueOf(value)
例如:
double value = 19.99;
BigDecimal number =
BigDecimal.valueOf(value);
但如果整个业务一开始就要求精确十进制:
最好不要先把金额保存成 double,再“补救”成 BigDecimal。
更合理的是从数据进入系统时就选择正确的数据模型。
3.6 BigDecimal 常用四则运算
BigDecimal 不能直接写:
a + b
进行 BigDecimal 数值相加。
它通过方法完成运算。
主要包括:
| 运算 | API |
| ---- | ------------ |
| 加法 | add() |
| 减法 | subtract() |
| 乘法 | multiply() |
| 除法 | divide() |
3.7 add():加法
BigDecimal price =
new BigDecimal("99.90");
BigDecimal shipping =
new BigDecimal("8.00");
BigDecimal total =
price.add(shipping);
System.out.println(total);
结果:
107.90
思维:
price + shipping
转换成 BigDecimal API:
price.add(shipping)
3.8 subtract():减法
BigDecimal original =
new BigDecimal("100.00");
BigDecimal discount =
new BigDecimal("20.50");
BigDecimal result =
original.subtract(discount);
System.out.println(result);
结果:
79.50
注意方向:
a.subtract(b)
表示:
a - b
3.9 multiply():乘法
例如商品单价:
19.90
购买:
3件
代码:
BigDecimal price =
new BigDecimal("19.90");
BigDecimal count =
new BigDecimal("3");
BigDecimal total =
price.multiply(count);
System.out.println(total);
得到:
59.70
3.10 divide():除法
简单情况:
BigDecimal a =
new BigDecimal("10");
BigDecimal b =
new BigDecimal("2");
BigDecimal result =
a.divide(b);
System.out.println(result);
得到:
5
但是除法有一个特殊问题。
3.11 为什么 1 / 3 会出现问题
例如:
BigDecimal one =
new BigDecimal("1");
BigDecimal three =
new BigDecimal("3");
BigDecimal result =
one.divide(three);
数学结果是:
0.333333333333...
这是无限循环小数。
BigDecimal 的核心目标恰恰是:
精确十进制运算。
但是:
1 / 3
无法用有限位十进制小数精确表示。
此时如果你没有告诉 BigDecimal:
到底保留几位?
如何舍入?
它无法凭空猜测你的业务规则。
因此会抛出:
ArithmeticException
3.12 指定 scale 和 RoundingMode
例如:
BigDecimal result =
BigDecimal.ONE.divide(
new BigDecimal("3"),
2,
RoundingMode.HALF_UP
);
结果:
0.33
参数:
divide(divisor, scale, roundingMode)
这里:
divisor
→ 除数
scale
→ 小数点后保留的位数
roundingMode
→ 舍入模式
3.13 RoundingMode.HALF_UP
需要:
import java.math.RoundingMode;
常见:
RoundingMode.HALF_UP
它接近我们在基础数学中熟悉的:
四舍五入。
例如:
BigDecimal number =
new BigDecimal("3.145");
BigDecimal result =
number.setScale(
2,
RoundingMode.HALF_UP
);
System.out.println(result);
得到:
3.15
3.14 不是所有业务都应该 HALF_UP
这一点非常重要。
BigDecimal 提供多种:
RoundingMode
例如:
UP
DOWN
CEILING
FLOOR
HALF_UP
HALF_DOWN
HALF_EVEN
UNNECESSARY
不同场景的规则不同。
例如:
银行规则
财务规则
税务规则
支付规则
统计规则
可能明确要求某一种舍入策略。
因此不要形成错误思维:
BigDecimal 舍入
=
永远 HALF_UP
正确原则是:
舍入模式属于业务规则的一部分。
3.15 setScale()
金额系统中经常需要:
保留两位小数
例如:
BigDecimal price =
new BigDecimal("19.999");
BigDecimal result =
price.setScale(
2,
RoundingMode.HALF_UP
);
System.out.println(result);
得到:
20.00
注意:
setScale()
依然不会修改原 BigDecimal。
3.16 BigDecimal 转换成 double
原始教学案例中:
BigDecimal result =
BigDecimal.valueOf(0.1)
.add(
BigDecimal.valueOf(0.2)
);
double value =
result.doubleValue();
可以通过:
doubleValue()
转成 double。
BigDecimal 作为 Number 的子类,还提供:
intValue()
longValue()
floatValue()
doubleValue()
等转换。
但要注意:
一旦重新转换成浮点类型,又重新进入浮点表示体系。
因此精确金额计算链路中,不应该毫无必要地频繁:
BigDecimal
→ double
→ BigDecimal
→ double
3.17 常量 ZERO、ONE、TEN
BigDecimal 提供常用常量:
BigDecimal.ZERO
BigDecimal.ONE
BigDecimal.TEN
例如:
BigDecimal balance =
BigDecimal.ZERO;
比:
new BigDecimal("0")
更清晰。
JDK 21 还提供:
BigDecimal.TWO
代表数值 2。
四、原理与进阶
4.1 BigDecimal 不是“无限精度计算机”
BigDecimal 常被描述为:
高精度小数。
更严谨地说,它能够表示:
任意精度的有符号十进制数。
但现实计算资源仍然有限。
例如:
- 内存有限
- CPU 时间有限
- scale 有范围
- 运算可能要求舍入
所以“任意精度”描述的是数据模型能力,而不是:
机器真的拥有无限内存和无限计算能力
4.2 precision 与 scale
两个非常重要的概念:
precision
有效数字总位数。
例如:
123.45
precision:
5
scale
小数点右侧位数。
123.45
scale:
2
例如:
BigDecimal value =
new BigDecimal("123.45");
System.out.println(
value.precision()
);
System.out.println(
value.scale()
);
得到:
5
2
不要把:
precision
和:
scale
混为一谈。
4.3 equals() 的陷阱
观察:
BigDecimal a =
new BigDecimal("2.0");
BigDecimal b =
new BigDecimal("2.00");
System.out.println(
a.equals(b)
);
结果:
false
很多初学者会疑惑:
2.0 明明等于 2.00
为什么 false?
因为 BigDecimal 的:
equals()
不仅考虑:
数值
还要求:
scale
一致。
这里:
2.0
scale = 1
2.00
scale = 2
因此:
a.equals(b)
为 false。
4.4 compareTo() 比较数值
如果业务真正想问:
这两个金额数值上是否一样?
通常更适合:
a.compareTo(b)
规则:
< 0
→ a < b
= 0
→ a 与 b 数值相等
> 0
→ a > b
例如:
BigDecimal a =
new BigDecimal("2.0");
BigDecimal b =
new BigDecimal("2.00");
System.out.println(
a.compareTo(b)
);
结果:
0
因为从数值意义:
2.0 == 2.00
4.5 BigDecimal 数值比较推荐模式
判断相等:
a.compareTo(b) == 0
判断大于:
a.compareTo(b) > 0
判断小于:
a.compareTo(b) < 0
例如判断余额是否足够:
if (balance.compareTo(price) >= 0) {
System.out.println("余额充足");
}
4.6 为什么不能使用 > 和 <
BigDecimal 是引用类型对象。
不能写:
if (a > b) {
}
>、< 不是 BigDecimal 对象之间的数值比较方案。
应该:
a.compareTo(b)
4.7 BigDecimal 与金额建模
即使使用 BigDecimal,也不意味着:
金融计算自动绝对正确。
例如金额计算还涉及:
货币单位
人民币还是美元
税率
折扣顺序
舍入时机
小数位规则
中间步骤是否舍入
最终步骤是否舍入
例如:
先折扣后税
和:
先税后折扣
甚至可能产生不同业务结果。
BigDecimal 解决的是:
十进制表示与运算工具。
业务正确性仍然需要正确的业务规则。
五、实践应用
5.1 商品金额计算
商品:
单价:19.90
数量:3
代码:
BigDecimal price =
new BigDecimal("19.90");
BigDecimal quantity =
new BigDecimal("3");
BigDecimal total =
price.multiply(quantity);
System.out.println(total);
结果:
59.70
5.2 折扣计算
假设:
原价:299.00
折扣:85%
可以:
BigDecimal price =
new BigDecimal("299.00");
BigDecimal discountRate =
new BigDecimal("0.85");
BigDecimal finalPrice =
price.multiply(discountRate)
.setScale(
2,
RoundingMode.HALF_UP
);
System.out.println(finalPrice);
5.3 商品结算完整案例
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PaymentDemo {
public static void main(String[] args) {
BigDecimal unitPrice =
new BigDecimal("39.90");
BigDecimal quantity =
new BigDecimal("3");
BigDecimal discount =
new BigDecimal("0.85");
BigDecimal subtotal =
unitPrice.multiply(quantity);
BigDecimal afterDiscount =
subtotal.multiply(discount);
BigDecimal finalAmount =
afterDiscount.setScale(
2,
RoundingMode.HALF_UP
);
System.out.println(
"原金额:" + subtotal
);
System.out.println(
"折后金额:" + finalAmount
);
}
}
核心数据流:
单价
×
数量
↓
原始金额
×
折扣率
↓
折后金额
↓
按照业务规则舍入
↓
最终金额
六、常见问题
6.1 BigDecimal 是不是专门给钱用的
不是。
它是:
任意精度十进制运算类型。
金额只是非常典型的应用场景。
凡是要求精确十进制运算的业务都可能使用。
6.2 为什么不能直接 new BigDecimal(0.1)
因为:
0.1
首先是一个不能精确表示十进制 0.1 的 double。
BigDecimal 构造器得到的是:
这个 double 实际二进制值的精确十进制展开。
不是数学意义上的精确 0.1。
6.3 new BigDecimal("0.1") 和 valueOf(0.1) 怎么选
两者都可以解决常见的:
想表达十进制 0.1
问题。
经验规则:
原始数据就是十进制文本
→ new BigDecimal(String)
手里已经是 double
→ BigDecimal.valueOf(double)
6.4 BigDecimal 为什么不能直接用 + - * /
因为:
BigDecimal
是对象。
常用:
add
subtract
multiply
divide
进行运算。
6.5 divide 为什么有时候报 ArithmeticException
例如:
1 / 3
结果无法使用有限十进制精确表示。
如果没有提供舍入策略,BigDecimal 不会自行猜测该保留多少位,因此可能抛出:
ArithmeticException
6.6 BigDecimal.equals 为什么会出现 2.0 不等于 2.00
因为:
equals()
同时比较:
数值
+
scale
而:
2.0 → scale 1
2.00 → scale 2
如果业务只比较数值大小,应考虑:
compareTo()
6.7 setScale 是否修改原对象
不会。
BigDecimal 是不可变对象。
错误:
price.setScale(
2,
RoundingMode.HALF_UP
);
然后还以为:
price
已经变化。
应该:
price =
price.setScale(
2,
RoundingMode.HALF_UP
);
6.8 doubleValue 后是不是还保持 BigDecimal 的十进制模型
不是。
转换成:
double
后,就重新遵循 double 的二进制浮点表示规则。
因此不要没有必要地转换。
七、练习与验收
7.1 知识问答
- 为什么
0.1 + 0.2使用 double 可能不是精确的0.3? - BigDecimal 主要解决什么问题?
- BigDecimal 位于哪个包?
new BigDecimal("0.1")与new BigDecimal(0.1)有什么区别?BigDecimal.valueOf(double)的作用是什么?add()、subtract()、multiply()、divide()分别对应什么运算?- 为什么
1 / 3使用无参数舍入策略的divide()可能抛出异常? - scale 是什么?
- precision 是什么?
- RoundingMode 是什么?
HALF_UP表示什么基本舍入思想?- 为什么舍入模式应该由业务规则决定?
- BigDecimal 是否可变?
equals()与compareTo()有什么差异?- 为什么
2.0与2.00的equals()可能为 false? - 如何判断两个 BigDecimal 数值是否相等?
- 为什么金融业务即使使用 BigDecimal 仍然需要明确业务规则?
7.2 代码阅读
不运行代码,预测结果:
import java.math.BigDecimal;
public class BigDecimalRead01 {
public static void main(String[] args) {
BigDecimal a =
new BigDecimal("0.1");
BigDecimal b =
new BigDecimal("0.2");
BigDecimal c =
a.add(b);
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
回答:
- 三行分别输出什么?
a是否发生变化?- 为什么?
继续阅读:
BigDecimal a =
new BigDecimal("2.0");
BigDecimal b =
new BigDecimal("2.00");
System.out.println(
a.equals(b)
);
System.out.println(
a.compareTo(b)
);
回答:
- 两个结果分别是什么?
- 为什么两个 API 会产生不同结果?
7.3 手写代码
任务一:基本四则运算
创建:
10.50
3.20
使用 BigDecimal 分别计算:
加
减
乘
除
除法保留:
2 位小数
任务二:商品结算
给定:
单价:29.90
数量:5
折扣:88%
计算最终金额。
要求:
- 全程使用 BigDecimal
- 最终保留两位小数
- 明确舍入模式
任务三:数值比较
创建:
100.0
100.00
99.99
分别使用:
equals()
compareTo()
进行比较,并解释结果。
7.4 Debug
下面代码存在精度设计问题:
BigDecimal price =
new BigDecimal(0.1);
BigDecimal count =
new BigDecimal(3);
BigDecimal total =
price.multiply(count);
System.out.println(total);
要求:
- 找出最值得修改的位置。
- 解释问题为什么在进入 BigDecimal 之前就已经产生。
- 修改代码。
继续 Debug:
BigDecimal a =
new BigDecimal("1");
BigDecimal b =
new BigDecimal("3");
System.out.println(
a.divide(b)
);
回答:
- 为什么可能出现异常?
- 如何通过明确精度和舍入模式解决?
7.5 综合训练
实现一个控制台购物结算程序。
已知:
商品A:19.90 × 3
商品B:39.50 × 2
会员折扣:90%
税率:6%
要求:
- 所有金额、比例使用 BigDecimal。
- 计算每类商品小计。
- 计算商品总价。
- 应用会员折扣。
- 根据明确规则计算税费。
- 计算最终金额。
- 最终金额保留两位小数。
- 使用
compareTo()判断最终金额是否超过 100 元。 - 说明每一步是否应该立即舍入,以及你的理由。
7.6 本章验收
能够闭卷写出:
new BigDecimal("0.1")
BigDecimal.valueOf(0.1)
add()
subtract()
multiply()
divide()
setScale()
compareTo()
能够解释:
double 为什么可能产生小数误差
能够解释:
为什么不要随便 new BigDecimal(0.1)
能够解释:
1 / 3 为什么需要舍入规则
能够准确区分:
equals()
vs
compareTo()
并能够独立完成:
商品金额
+
数量
+
折扣
+
税费
+
舍入
这一完整 BigDecimal 运算链,则本章核心能力基本达标。