BigDecimal精确计算完全指南
适合对象:Java开发者(初级至中级)
引言
在开始之前,我想问一个问题:0.1 + 0.2 等于多少?
你可能会说:这还不简单,当然是0.3!
但是,让我们在Java中验证一下:
public class Test {
public static void main(String[] args) {
double result = 0.1 + 0.2;
System.out.println(result);
System.out.println(result == 0.3);
}
}
运行结果:
0.30000000000000004
false
看到了吗?计算机告诉我们,0.1 + 0.2 并不等于0.3!
这就是我们今天要解决的问题:如何在Java中进行精确的小数计算。
第一章:为什么需要BigDecimal
1.1 浮点数的精度问题
首先,我们需要理解为什么和
double会有精度问题。
float
原理解释:
计算机使用二进制来存储数字,但很多十进制小数无法用二进制精确表示。就像十进制的1/3(0.3333…)无法用有限位数表示一样,十进制的0.1在二进制中也是无限循环小数。
十进制 0.1 → 二进制 0.0001100110011001100110011001100110011...(无限循环)
由于计算机存储空间有限,只能截断,因此产生了精度误差。
实际测试:
public class FloatPrecisionTest {
public static void main(String[] args) {
// 测试1:简单加法
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
// 测试2:连续加法
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
System.out.println("0.1 累加10次 = " + sum);
System.out.println("是否等于1.0? " + (sum == 1.0));
// 测试3:金额计算
double price = 0.03;
double quantity = 3;
double total = price * quantity;
System.out.println("0.03 × 3 = " + total);
}
}
输出结果:
0.1 + 0.2 = 0.30000000000000004
0.1 累加10次 = 0.9999999999999999
是否等于1.0? false
0.03 × 3 = 0.09000000000000001
1.2 在金融系统中的风险
想象一下,如果你在开发一个银行系统,用户有1000元存款,年利率3%:
// ❌ 使用double计算利息
double principal = 1000.0;
double rate = 0.03;
double interest = principal * rate;
System.out.println("利息:" + interest); // 30.000000000000004
// 如果要入库,四舍五入后可能丢失精度
// 如果有百万用户,累计误差会很大!
结论: 在涉及金额、数量等需要精确计算的场景,绝对不能使用float或double,必须使用。
BigDecimal
第二章:BigDecimal的正确使用方法
2.1 创建BigDecimal对象
重要原则:永远不要用double来构造BigDecimal!
public class BigDecimalCreation {
public static void main(String[] args) {
// ❌ 错误方式1:用double构造
BigDecimal wrong = new BigDecimal(0.1);
System.out.println("用double构造:" + wrong);
// 输出:0.1000000000000000055511151231257827021181583404541015625
// 为什么?因为0.1的double表示本身就不精确!
// ✅ 正确方式1:用String构造(推荐)
BigDecimal correct1 = new BigDecimal("0.1");
System.out.println("用String构造:" + correct1);
// 输出:0.1
// ✅ 正确方式2:用BigDecimal.valueOf()
BigDecimal correct2 = BigDecimal.valueOf(0.1);
System.out.println("用valueOf:" + correct2);
// 输出:0.1
// valueOf内部会先将double转为String,再构造
// ✅ 正确方式3:整数可以直接构造
BigDecimal correct3 = new BigDecimal(100);
System.out.println("用int构造:" + correct3);
// 输出:100
}
}
总结:
最佳实践:用构造 →
String可以接受:用
new BigDecimal("0.1") →
valueOf()绝不使用:用
BigDecimal.valueOf(0.1)构造 →
double
new BigDecimal(0.1)
2.2 四则运算
BigDecimal不能直接用 、
+、
-、
* 运算符,必须调用方法。
/
public class BigDecimalArithmetic {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("3.2");
// 加法
BigDecimal sum = a.add(b);
System.out.println("加法:" + a + " + " + b + " = " + sum);
// 减法
BigDecimal diff = a.subtract(b);
System.out.println("减法:" + a + " - " + b + " = " + diff);
// 乘法
BigDecimal product = a.multiply(b);
System.out.println("乘法:" + a + " × " + b + " = " + product);
// 除法(重点!)
BigDecimal quotient = a.divide(b, 2, RoundingMode.HALF_UP);
System.out.println("除法:" + a + " ÷ " + b + " = " + quotient);
}
}
输出:
加法:10.5 + 3.2 = 13.7
减法:10.5 - 3.2 = 7.3
乘法:10.5 × 3.2 = 33.60
除法:10.5 ÷ 3.2 = 3.28
注意事项:
BigDecimal是不可变类,每次运算都会返回新对象原对象不会被修改除法必须特别注意(下一节详解)
2.3 除法的陷阱与解决
问题演示:
public class DivisionTrap {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// ❌ 这样会抛异常!
try {
BigDecimal result = a.divide(b);
} catch (ArithmeticException e) {
System.out.println("异常:" + e.getMessage());
// Non-terminating decimal expansion; no exact representable decimal result.
}
}
}
为什么会抛异常?
因为10 ÷ 3 = 3.33333…(无限循环小数),BigDecimal无法用有限位数精确表示,所以抛出异常。
正确的做法:
public class DivisionCorrect {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// ✅ 方式1:指定精度和舍入模式
BigDecimal result1 = a.divide(b, 2, RoundingMode.HALF_UP);
System.out.println("保留2位小数,四舍五入:" + result1); // 3.33
// ✅ 方式2:使用MathContext
MathContext mc = new MathContext(10, RoundingMode.HALF_UP);
BigDecimal result2 = a.divide(b, mc);
System.out.println("保留10位有效数字:" + result2); // 3.333333333
// ✅ 方式3:整除(返回商和余数)
BigDecimal[] divideAndRemainder = a.divideAndRemainder(b);
System.out.println("商:" + divideAndRemainder[0]); // 3
System.out.println("余数:" + divideAndRemainder[1]); // 1
}
}
常用的舍入模式(RoundingMode):
| 模式 | 说明 | 示例(保留1位) |
|---|---|---|
|
四舍五入(推荐) | 1.25 → 1.3, 1.24 → 1.2 |
|
五舍六入 | 1.25 → 1.2, 1.26 → 1.3 |
|
向上取整(远离零) | 1.21 → 1.3, -1.21 → -1.3 |
|
向下取整(趋向零) | 1.29 → 1.2, -1.29 → -1.2 |
|
向正无穷方向取整 | 1.21 → 1.3, -1.21 → -1.2 |
|
向负无穷方向取整 | 1.29 → 1.2, -1.29 → -1.3 |
推荐: 金融计算通常使用 (四舍五入)
RoundingMode.HALF_UP
2.4 比较大小
重要:BigDecimal比较不能用 == 或 equals()!
public class BigDecimalComparison {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
// ❌ 错误方式1:使用 ==
System.out.println("a == b: " + (a == b)); // false(比较引用)
// ❌ 错误方式2:使用 equals()
System.out.println("a.equals(b): " + a.equals(b)); // false!
// 为什么?因为equals()会比较scale(精度)
// a的scale是1,b的scale是2,所以不相等
// ✅ 正确方式:使用 compareTo()
System.out.println("a.compareTo(b): " + a.compareTo(b)); // 0(相等)
// compareTo()返回值:
// -1:小于
// 0:等于
// 1:大于
if (a.compareTo(b) == 0) {
System.out.println("a 等于 b");
} else if (a.compareTo(b) < 0) {
System.out.println("a 小于 b");
} else {
System.out.println("a 大于 b");
}
}
}
总结:
判断相等:用 判断大于:用
compareTo(other) == 0判断小于:用
compareTo(other) > 0
compareTo(other) < 0
2.5 精度控制
public class BigDecimalScale {
public static void main(String[] args) {
BigDecimal num = new BigDecimal("123.456789");
// 设置精度(保留小数位数)
BigDecimal scaled1 = num.setScale(2, RoundingMode.HALF_UP);
System.out.println("保留2位小数:" + scaled1); // 123.46
BigDecimal scaled2 = num.setScale(4, RoundingMode.HALF_UP);
System.out.println("保留4位小数:" + scaled2); // 123.4568
// 去除末尾的0
BigDecimal num2 = new BigDecimal("100.000");
System.out.println("原始值:" + num2); // 100.000
System.out.println("去除末尾0:" + num2.stripTrailingZeros()); // 100
// 获取精度信息
System.out.println("scale(小数位数):" + num.scale()); // 6
System.out.println("precision(总位数):" + num.precision()); // 9
}
}
第三章:实战案例分析
3.1 案例1:电商订单金额计算
需求: 计算订单总金额,要求精确到分。
public class OrderCalculation {
public static void main(String[] args) {
// 商品单价(元)
BigDecimal price1 = new BigDecimal("99.99");
BigDecimal price2 = new BigDecimal("158.80");
// 商品数量
BigDecimal quantity1 = new BigDecimal("3");
BigDecimal quantity2 = new BigDecimal("2");
// 计算小计
BigDecimal subtotal1 = price1.multiply(quantity1);
BigDecimal subtotal2 = price2.multiply(quantity2);
System.out.println("商品1小计:" + subtotal1);
System.out.println("商品2小计:" + subtotal2);
// 计算总金额
BigDecimal total = subtotal1.add(subtotal2);
System.out.println("订单总金额:" + total);
// 优惠折扣:9折
BigDecimal discount = new BigDecimal("0.9");
BigDecimal finalAmount = total.multiply(discount)
.setScale(2, RoundingMode.HALF_UP); // 保留2位小数
System.out.println("优惠后金额:" + finalAmount);
}
}
输出:
商品1小计:299.97
商品2小计:317.60
订单总金额:617.57
优惠后金额:555.81
3.2 案例2:银行存款利息计算
需求: 计算定期存款利息,本金10000元,年利率2.75%,存期1年。
public class InterestCalculation {
public static void main(String[] args) {
// 本金
BigDecimal principal = new BigDecimal("10000");
// 年利率(2.75%)
BigDecimal annualRate = new BigDecimal("0.0275");
// 存期(天数)
int days = 365;
// 计算利息:本金 × 年利率 × 天数 / 365
BigDecimal interest = principal
.multiply(annualRate)
.multiply(new BigDecimal(days))
.divide(new BigDecimal("365"), 2, RoundingMode.HALF_UP);
System.out.println("本金:" + principal + " 元");
System.out.println("年利率:" + annualRate.multiply(new BigDecimal("100")) + "%");
System.out.println("存期:" + days + " 天");
System.out.println("利息:" + interest + " 元");
// 本息合计
BigDecimal total = principal.add(interest);
System.out.println("本息合计:" + total + " 元");
}
}
输出:
本金:10000 元
年利率:2.75%
存期:365 天
利息:275.00 元
本息合计:10275.00 元
3.3 案例3:税费计算
需求: 计算商品含税价格,税率13%。
public class TaxCalculation {
public static void main(String[] args) {
// 不含税价格
BigDecimal priceExcludingTax = new BigDecimal("1000.00");
// 税率 13%
BigDecimal taxRate = new BigDecimal("0.13");
// 计算税额
BigDecimal taxAmount = priceExcludingTax
.multiply(taxRate)
.setScale(2, RoundingMode.HALF_UP);
// 含税价格
BigDecimal priceIncludingTax = priceExcludingTax.add(taxAmount);
System.out.println("不含税价格:" + priceExcludingTax + " 元");
System.out.println("税率:" + taxRate.multiply(new BigDecimal("100")) + "%");
System.out.println("税额:" + taxAmount + " 元");
System.out.println("含税价格:" + priceIncludingTax + " 元");
// 反向计算:从含税价格计算不含税价格
BigDecimal reversePrice = priceIncludingTax
.divide(BigDecimal.ONE.add(taxRate), 2, RoundingMode.HALF_UP);
System.out.println("
反向计算不含税价格:" + reversePrice + " 元");
}
}
输出:
不含税价格:1000.00 元
税率:13%
税额:130.00 元
含税价格:1130.00 元
反向计算不含税价格:1000.00 元
第四章:常见错误与最佳实践
4.1 常见错误总结
错误1:使用double构造
// ❌ 错误
BigDecimal bd = new BigDecimal(0.1);
// ✅ 正确
BigDecimal bd = new BigDecimal("0.1");
错误2:除法不指定精度
// ❌ 错误(会抛异常)
BigDecimal result = a.divide(b);
// ✅ 正确
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);
错误3:使用equals()比较
// ❌ 错误
if (a.equals(b)) { }
// ✅ 正确
if (a.compareTo(b) == 0) { }
错误4:忽略不可变性
BigDecimal amount = new BigDecimal("100");
amount.add(new BigDecimal("50")); // ❌ 这样没有效果
System.out.println(amount); // 仍然是100
// ✅ 正确
amount = amount.add(new BigDecimal("50"));
System.out.println(amount); // 150
4.2 最佳实践
实践1:封装工具类
public class BigDecimalUtil {
/**
* 加法
*/
public static BigDecimal add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2);
}
/**
* 减法
*/
public static BigDecimal subtract(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2);
}
/**
* 乘法
*/
public static BigDecimal multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2);
}
/**
* 除法
* @param scale 保留小数位数
*/
public static BigDecimal divide(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("精度不能小于0");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_UP);
}
/**
* 比较大小
* @return v1 > v2 返回1, v1 = v2 返回0, v1 < v2 返回-1
*/
public static int compare(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.compareTo(b2);
}
}
实践2:金额类封装
public class Money {
private BigDecimal amount;
public Money(String amount) {
this.amount = new BigDecimal(amount);
}
public Money add(Money other) {
return new Money(this.amount.add(other.amount).toString());
}
public Money subtract(Money other) {
return new Money(this.amount.subtract(other.amount).toString());
}
public Money multiply(BigDecimal multiplier) {
return new Money(this.amount.multiply(multiplier).toString());
}
public Money divide(BigDecimal divisor, int scale) {
BigDecimal result = this.amount.divide(divisor, scale, RoundingMode.HALF_UP);
return new Money(result.toString());
}
public boolean isGreaterThan(Money other) {
return this.amount.compareTo(other.amount) > 0;
}
public boolean isLessThan(Money other) {
return this.amount.compareTo(other.amount) < 0;
}
public boolean isEqualTo(Money other) {
return this.amount.compareTo(other.amount) == 0;
}
@Override
public String toString() {
return amount.setScale(2, RoundingMode.HALF_UP).toString();
}
}
4.3 性能优化建议
建议1:复用常量
public class Constants {
public static final BigDecimal ZERO = BigDecimal.ZERO;
public static final BigDecimal ONE = BigDecimal.ONE;
public static final BigDecimal TEN = BigDecimal.TEN;
public static final BigDecimal HUNDRED = new BigDecimal("100");
// 常用税率
public static final BigDecimal TAX_RATE_13 = new BigDecimal("0.13");
public static final BigDecimal TAX_RATE_6 = new BigDecimal("0.06");
}
建议2:批量计算优化
// ❌ 效率低
BigDecimal sum = BigDecimal.ZERO;
for (BigDecimal num : numbers) {
sum = sum.add(num); // 每次创建新对象
}
// ✅ 推荐(如果数据量大)
BigDecimal sum = numbers.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
第五章:高级主题
5.1 BigDecimal与数据库
在数据库中存储金额时的建议:
-- ✅ 推荐方式1:使用DECIMAL类型
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
amount DECIMAL(19, 2) -- 19位总长度,2位小数
);
-- ✅ 推荐方式2:存储为分(整数)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
amount_cents BIGINT -- 以分为单位存储
);
Java代码:
// 方式1:直接存储BigDecimal
public void saveOrder(Order order) {
String sql = "INSERT INTO orders (id, amount) VALUES (?, ?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, order.getId());
ps.setBigDecimal(2, order.getAmount()); // 直接设置BigDecimal
ps.executeUpdate();
}
// 方式2:转为分存储
public void saveOrder(Order order) {
BigDecimal amountInYuan = order.getAmount();
long amountInCents = amountInYuan
.multiply(new BigDecimal("100"))
.longValue();
String sql = "INSERT INTO orders (id, amount_cents) VALUES (?, ?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, order.getId());
ps.setLong(2, amountInCents);
ps.executeUpdate();
}
5.2 BigDecimal与JSON序列化
// 使用Jackson
public class Order {
private Long id;
@JsonSerialize(using = ToStringSerializer.class) // 序列化为字符串
private BigDecimal amount;
// getters and setters
}
// 输出JSON:
// {"id": 1001, "amount": "99.99"}
5.3 BigDecimal的内部实现
了解原理,用得更好:
BigDecimal内部由两部分组成:
:未缩放的整数值
BigInteger intVal:小数点后的位数
int scale
例如: 在内部表示为:
123.45
intVal = 12345
scale = 2
这就是为什么会比较scale的原因:
equals()
new BigDecimal("1.0") // intVal=10, scale=1
new BigDecimal("1.00") // intVal=100, scale=2
// 虽然值相等,但表示不同!
总结
✅ 必须记住的5条铁律
永远不要用double构造BigDecimal
// ❌ new BigDecimal(0.1)
// ✅ new BigDecimal("0.1")
除法必须指定精度和舍入模式
result = a.divide(b, 2, RoundingMode.HALF_UP);
比较大小用compareTo,不用equals
if (a.compareTo(b) == 0) { }
BigDecimal是不可变的
amount = amount.add(other); // 必须接收返回值
金融系统必须用BigDecimal
// 涉及金额,一律用BigDecimal
📌 适用场景
✅ 金融系统(银行、支付、理财)✅ 电商系统(订单、价格)✅ 会计系统(账目、报表)✅ 任何需要精确小数计算的场景
有任何疑问,欢迎随时提问!


