问题
为什么不能用浮点数表示金额?
答案
核心原因
浮点数(float/double)采用二进制浮点表示,大部分十进制小数无法精确存储,导致计算误差累积,不满足金融场景的精度要求。
精度问题演示
1. 存储误差
double price = 0.1;
System.out.println(price); // 0.1(显示格式化后)
// 实际存储值
System.out.println(new BigDecimal(price));
// 输出:0.1000000000000000055511151231257827021181583404541015625
// ❌ 不是精确的0.1
2. 计算误差
// 场景:计算 0.1 + 0.2
double result1 = 0.1 + 0.2;
System.out.println(result1); // 0.30000000000000004 ❌
// 场景:货币计算
double balance = 1.0;
double cost = 0.9;
double remaining = balance - cost;
System.out.println(remaining); // 0.09999999999999998 ❌
// 场景:循环累加
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
System.out.println(sum); // 0.9999999999999999 ❌ 期望1.0
3. 比较失效
double a = 0.1 + 0.2; // 0.30000000000000004
double b = 0.3; // 0.3
System.out.println(a == b); // false ❌
System.out.println(a); // 0.30000000000000004
System.out.println(b); // 0.3
根本原理
IEEE 754标准
double使用64位二进制存储:
[符号位:1位][指数:11位][尾数:52位]
问题:十进制小数转二进制时产生无限循环:
十进制 0.1 = 1/10
二进制表示 = 0.0001100110011001100110011... (无限循环)
存储时截断 → 精度丢失
示例:0.1的二进制表示
0.1(十进制)
= 1/16 + 1/32 + 1/256 + 1/512 + ...
= 0.0001100110011001100110011...(二进制)
double存储(52位尾数):
0.00011001100110011001100110011001100110011001100110011010
↑ 截断后的值 ≈ 0.1000000000000000055511...
金融场景的风险
1. 累计误差
// 场景:计算100笔0.01元的交易
double total = 0.0;
for (int i = 0; i < 100; i++) {
total += 0.01;
}
System.out.println(total); // 0.9999999999999999 ❌
// 实际应该是1.0,误差虽小但不可接受
2. 舍入错误
// 场景:优惠券计算
double originalPrice = 29.9;
double discount = 0.85; // 85折
double finalPrice = originalPrice * discount;
System.out.println(finalPrice);
// 25.415000000000003 ❌ 产生了0.000000000000003的误差
// 四舍五入到分
System.out.println(Math.round(finalPrice * 100) / 100.0);
// 25.42,但中间过程已有误差累积
3. 账户不平
// 场景:多人分账
double totalAmount = 100.0;
int people = 3;
double perPerson = totalAmount / people; // 33.33333333333333
double distributed = perPerson * people;
System.out.println(distributed); // 99.99999999999999 ❌
// 分账后总和不等于原始金额!
正确方案:BigDecimal
// ✅ 使用BigDecimal
BigDecimal price1 = new BigDecimal("0.1");
BigDecimal price2 = new BigDecimal("0.2");
BigDecimal sum = price1.add(price2);
System.out.println(sum); // 0.3 ✅ 精确
// ✅ 循环累加
BigDecimal total = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
total = total.add(new BigDecimal("0.1"));
}
System.out.println(total); // 1.0 ✅
BigDecimal最佳实践
1. 构造器选择
// ❌ 错误:使用double构造器
BigDecimal wrong = new BigDecimal(0.1);
// 0.1000000000000000055511151231257827021181583404541015625
// ✅ 正确:使用String构造器
BigDecimal correct = new BigDecimal("0.1"); // 0.1
// ✅ 或使用valueOf
BigDecimal correct2 = BigDecimal.valueOf(0.1); // 内部调用Double.toString
2. 运算操作
BigDecimal a = new BigDecimal("100.00");
BigDecimal b = new BigDecimal("30.50");
// 加法
BigDecimal sum = a.add(b); // 130.50
// 减法
BigDecimal diff = a.subtract(b); // 69.50
// 乘法
BigDecimal product = a.multiply(b); // 3050.0000
// 除法(必须指定精度和舍入模式)
BigDecimal quotient = a.divide(b, 2, RoundingMode.HALF_UP); // 3.28
3. 除法陷阱
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// ❌ 错误:不指定精度会抛异常
try {
BigDecimal result = a.divide(b); // ArithmeticException
} catch (ArithmeticException e) {
// Non-terminating decimal expansion; no exact representable decimal result.
}
// ✅ 正确:指定精度和舍入模式
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33
4. 舍入模式
BigDecimal value = new BigDecimal("2.355");
// HALF_UP:四舍五入
value.setScale(2, RoundingMode.HALF_UP); // 2.36
// HALF_DOWN:五舍六入
value.setScale(2, RoundingMode.HALF_DOWN); // 2.35
// DOWN:直接截断
value.setScale(2, RoundingMode.DOWN); // 2.35
// UP:向上取整
value.setScale(2, RoundingMode.UP); // 2.36
替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| BigDecimal | 精度完美、API丰富 | 性能较慢、代码冗长 | 金额计算(首选) |
| 整数(分) | 性能好、简单 | 需手动处理小数点 | 高性能要求场景 |
| 第三方库 | 如Joda-Money | 引入依赖 | 复杂金融系统 |
整数方案示例
// 用整数(分)表示金额
public class Money {
private final long cents; // 以分为单位
public Money(String amount) {
// "10.50" → 1050分
this.cents = new BigDecimal(amount)
.multiply(new BigDecimal("100"))
.longValue();
}
public Money add(Money other) {
return new Money(this.cents + other.cents);
}
public String toString() {
return String.format("%.2f", cents / 100.0);
}
}
// 使用
Money m1 = new Money("10.50");
Money m2 = new Money("5.30");
Money total = m1.add(m2); // 15.80
实战案例
订单金额计算
public class Order {
private List<OrderItem> items;
// ✅ 正确:使用BigDecimal
public BigDecimal calculateTotal() {
return items.stream()
.map(item -> item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// ❌ 错误:使用double
public double calculateTotalWrong() {
return items.stream()
.mapToDouble(item -> item.getPriceDouble() * item.getQuantity())
.sum(); // 精度丢失
}
}
数据库存储
-- ✅ 正确:使用DECIMAL类型
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
total_amount DECIMAL(19, 2) -- 总额最多17位整数,2位小数
);
-- ❌ 错误:使用DOUBLE
CREATE TABLE orders_wrong (
total_amount DOUBLE -- 精度问题
);
// Entity定义
@Entity
public class Order {
@Column(precision = 19, scale = 2)
private BigDecimal totalAmount; // ✅ 正确
// private double totalAmount; // ❌ 错误
}
性能考量
// JMH基准测试(百万次操作)
@Benchmark
public double testDoubleAdd() {
double sum = 0.0;
for (int i = 0; i < 1000; i++) {
sum += 0.01;
}
return sum;
}
@Benchmark
public BigDecimal testBigDecimalAdd() {
BigDecimal sum = BigDecimal.ZERO;
BigDecimal increment = new BigDecimal("0.01");
for (int i = 0; i < 1000; i++) {
sum = sum.add(increment);
}
return sum;
}
// 典型结果
// testDoubleAdd: 2 μs/op
// testBigDecimalAdd: 50 μs/op (慢25倍)
结论:BigDecimal慢20-30倍,但金额计算精度优先于性能。
答题总结
浮点数采用二进制表示,无法精确存储大部分十进制小数(如0.1),导致存储误差和计算误差累积。金融场景不能容忍任何精度损失,必须使用 BigDecimal(String构造器)进行金额计算。典型问题包括:0.1+0.2=0.30000000000000004、累加误差、账户不平等。最佳实践:
- 使用
new BigDecimal("0.1")构造 - 除法必须指定精度和舍入模式
- 数据库使用DECIMAL类型
- 高性能场景可考虑整数(分)方案