标题: 金额计算还在用Double?BigDecimal才是王道!
副标题: 从精度丢失到分为单位,彻底解决金额计算问题
🎬 开篇:一次致命的精度丢失
某电商平台优惠券系统:
用户下单:100元
优惠券:8.8折
应付金额:100 * 0.88 = 88.0元
代码实现(错误):
double price = 100.0;
double discount = 0.88;
double finalPrice = price * discount;
// 结果:87.99999999999999 💀
用户支付:87.99元
实际应付:88.00元
差额:0.01元
结果:
- 10万订单,累计差额1000元
- 财务对账不平
- 用户投诉
- 公司损失 💸
老板:为什么会这样?!
开发:我用了double... 😭
改用BigDecimal后:
- 精度完全准确
- 对账零差异
- 老板放心了 😊
教训:金额计算绝对不能用float/double!
🤔 为什么不能用Float/Double?
public class FloatProblem {
public static void main(String[] args) {
// ❌ 错误示范
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(1.0 - 0.9); // 0.09999999999999998
System.out.println(4.015 * 100); // 401.49999999999994
// 商业计算的灾难
double price = 2.0;
double discount = 0.1;
double finalPrice = price - discount;
System.out.println(finalPrice); // 1.9000000000000001 💀
}
}
原因: Float/Double使用IEEE 754标准,二进制无法精确表示某些十进制小数!
📚 知识地图
金额计算解决方案
├── ✅ 正确方案
│ ├── BigDecimal(推荐)⭐⭐⭐⭐⭐
│ ├── 分为单位(整数运算)⭐⭐⭐⭐⭐
│ └── 第三方库(Joda-Money)⭐⭐⭐
├── ⚡ 核心要点
│ ├── 精度控制
│ ├── 舍入模式
│ ├── 比较判断
│ └── 格式化输出
└── 🎯 最佳实践
├── 数据库存储(DECIMAL)
├── 接口传输(字符串)
├── 计算处理(BigDecimal)
└── 日志记录(格式化)
✅ 方案1:BigDecimal(推荐)
基础使用
/**
* BigDecimal基础用法
*/
public class BigDecimalBasic {
public static void main(String[] args) {
// ✅ 正确的创建方式
BigDecimal price1 = new BigDecimal("100.00"); // 字符串(推荐)
BigDecimal price2 = BigDecimal.valueOf(100.00); // valueOf方法
// ❌ 错误的创建方式
BigDecimal price3 = new BigDecimal(0.1); // double构造(精度丢失!)
System.out.println(price3); // 0.1000000000000000055511151231257827021181583404541015625
// ✅ 正确示例
BigDecimal correct = new BigDecimal("0.1");
System.out.println(correct); // 0.1
// 基本运算
BigDecimal a = new BigDecimal("100.00");
BigDecimal b = new BigDecimal("88.00");
System.out.println("加法:" + a.add(b)); // 188.00
System.out.println("减法:" + a.subtract(b)); // 12.00
System.out.println("乘法:" + a.multiply(b)); // 8800.0000
System.out.println("除法:" + a.divide(b, 2, RoundingMode.HALF_UP)); // 1.14
}
}
核心工具类
/**
* 金额计算工具类
*/
public class MoneyUtils {
/**
* 默认精度(小数位数)
*/
private static final int DEFAULT_SCALE = 2;
/**
* 默认舍入模式(四舍五入)
*/
private static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_UP;
/**
* 加法
*/
public static BigDecimal add(BigDecimal a, BigDecimal b) {
if (a == null) {
a = BigDecimal.ZERO;
}
if (b == null) {
b = BigDecimal.ZERO;
}
return a.add(b);
}
/**
* 减法
*/
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
if (a == null) {
a = BigDecimal.ZERO;
}
if (b == null) {
b = BigDecimal.ZERO;
}
return a.subtract(b);
}
/**
* 乘法(自动保留2位小数)
*/
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
if (a == null || b == null) {
return BigDecimal.ZERO;
}
return a.multiply(b).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING_MODE);
}
/**
* 除法(自动保留2位小数)
*/
public static BigDecimal divide(BigDecimal a, BigDecimal b) {
if (a == null || b == null || b.compareTo(BigDecimal.ZERO) == 0) {
throw new IllegalArgumentException("除数不能为null或0");
}
return a.divide(b, DEFAULT_SCALE, DEFAULT_ROUNDING_MODE);
}
/**
* 除法(自定义精度)
*/
public static BigDecimal divide(BigDecimal a, BigDecimal b, int scale) {
if (a == null || b == null || b.compareTo(BigDecimal.ZERO) == 0) {
throw new IllegalArgumentException("除数不能为null或0");
}
return a.divide(b, scale, DEFAULT_ROUNDING_MODE);
}
/**
* 比较大小
* @return a > b 返回1,a == b 返回0,a < b 返回-1
*/
public static int compare(BigDecimal a, BigDecimal b) {
if (a == null) {
a = BigDecimal.ZERO;
}
if (b == null) {
b = BigDecimal.ZERO;
}
return a.compareTo(b);
}
/**
* 是否相等(推荐用compareTo,不要用equals)
*/
public static boolean equals(BigDecimal a, BigDecimal b) {
if (a == null) {
a = BigDecimal.ZERO;
}
if (b == null) {
b = BigDecimal.ZERO;
}
// ⚡ 使用compareTo比较,忽略精度差异
// new BigDecimal("1.0").equals(new BigDecimal("1.00")) = false
// new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) = 0
return a.compareTo(b) == 0;
}
/**
* 是否大于
*/
public static boolean greaterThan(BigDecimal a, BigDecimal b) {
return compare(a, b) > 0;
}
/**
* 是否大于等于
*/
public static boolean greaterThanOrEqual(BigDecimal a, BigDecimal b) {
return compare(a, b) >= 0;
}
/**
* 是否小于
*/
public static boolean lessThan(BigDecimal a, BigDecimal b) {
return compare(a, b) < 0;
}
/**
* 是否小于等于
*/
public static boolean lessThanOrEqual(BigDecimal a, BigDecimal b) {
return compare(a, b) <= 0;
}
/**
* 格式化金额(保留2位小数)
*/
public static String format(BigDecimal amount) {
if (amount == null) {
amount = BigDecimal.ZERO;
}
DecimalFormat df = new DecimalFormat("#,##0.00");
return df.format(amount);
}
/**
* 格式化金额(带货币符号)
*/
public static String formatWithSymbol(BigDecimal amount) {
if (amount == null) {
amount = BigDecimal.ZERO;
}
DecimalFormat df = new DecimalFormat("¥#,##0.00");
return df.format(amount);
}
/**
* 字符串转BigDecimal
*/
public static BigDecimal parse(String amount) {
if (StringUtils.isBlank(amount)) {
return BigDecimal.ZERO;
}
try {
// 去除千分位逗号和货币符号
amount = amount.replaceAll("[,¥$€£]", "").trim();
return new BigDecimal(amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("金额格式错误:" + amount, e);
}
}
/**
* 保留指定小数位数
*/
public static BigDecimal setScale(BigDecimal amount, int scale) {
if (amount == null) {
return BigDecimal.ZERO;
}
return amount.setScale(scale, DEFAULT_ROUNDING_MODE);
}
/**
* 保留2位小数(默认)
*/
public static BigDecimal setScale(BigDecimal amount) {
return setScale(amount, DEFAULT_SCALE);
}
/**
* 百分比计算
* 例:100 * 88% = 88.00
*/
public static BigDecimal percentage(BigDecimal amount, BigDecimal percent) {
if (amount == null || percent == null) {
return BigDecimal.ZERO;
}
// 将百分比转换为小数(88% -> 0.88)
BigDecimal rate = divide(percent, new BigDecimal("100"));
return multiply(amount, rate);
}
/**
* 折扣计算
* 例:100 * 8.8折 = 88.00
*/
public static BigDecimal discount(BigDecimal amount, BigDecimal discount) {
if (amount == null || discount == null) {
return BigDecimal.ZERO;
}
// 将折扣转换为小数(8.8折 -> 0.88)
BigDecimal rate = divide(discount, BigDecimal.TEN);
return multiply(amount, rate);
}
/**
* 最小值
*/
public static BigDecimal min(BigDecimal a, BigDecimal b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.compareTo(b) < 0 ? a : b;
}
/**
* 最大值
*/
public static BigDecimal max(BigDecimal a, BigDecimal b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
return a.compareTo(b) > 0 ? a : b;
}
/**
* 求和
*/
public static BigDecimal sum(BigDecimal... amounts) {
if (amounts == null || amounts.length == 0) {
return BigDecimal.ZERO;
}
BigDecimal total = BigDecimal.ZERO;
for (BigDecimal amount : amounts) {
if (amount != null) {
total = total.add(amount);
}
}
return total;
}
/**
* 求和(集合)
*/
public static BigDecimal sum(List<BigDecimal> amounts) {
if (amounts == null || amounts.isEmpty()) {
return BigDecimal.ZERO;
}
return amounts.stream()
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
🎯 舍入模式详解
/**
* 舍入模式示例
*/
public class RoundingModeExample {
public static void main(String[] args) {
BigDecimal value = new BigDecimal("2.355");
// HALF_UP:四舍五入(最常用)⭐⭐⭐⭐⭐
System.out.println("HALF_UP: " +
value.setScale(2, RoundingMode.HALF_UP)); // 2.36
// HALF_DOWN:五舍六入
System.out.println("HALF_DOWN: " +
value.setScale(2, RoundingMode.HALF_DOWN)); // 2.35
// HALF_EVEN:银行家舍入法(四舍六入五取偶)
BigDecimal v1 = new BigDecimal("2.355");
BigDecimal v2 = new BigDecimal("2.365");
System.out.println("HALF_EVEN (2.355): " +
v1.setScale(2, RoundingMode.HALF_EVEN)); // 2.36(5前面是5,向上)
System.out.println("HALF_EVEN (2.365): " +
v2.setScale(2, RoundingMode.HALF_EVEN)); // 2.36(5前面是6,向上)
// UP:向上取整(远离0)
System.out.println("UP: " +
value.setScale(2, RoundingMode.UP)); // 2.36
// DOWN:向下取整(靠近0,直接截断)
System.out.println("DOWN: " +
value.setScale(2, RoundingMode.DOWN)); // 2.35
// CEILING:向正无穷方向取整
BigDecimal positive = new BigDecimal("2.351");
BigDecimal negative = new BigDecimal("-2.351");
System.out.println("CEILING (positive): " +
positive.setScale(2, RoundingMode.CEILING)); // 2.36
System.out.println("CEILING (negative): " +
negative.setScale(2, RoundingMode.CEILING)); // -2.35
// FLOOR:向负无穷方向取整
System.out.println("FLOOR (positive): " +
positive.setScale(2, RoundingMode.FLOOR)); // 2.35
System.out.println("FLOOR (negative): " +
negative.setScale(2, RoundingMode.FLOOR)); // -2.36
// UNNECESSARY:不需要舍入(如果有精度丢失会抛异常)
BigDecimal exact = new BigDecimal("2.35");
System.out.println("UNNECESSARY: " +
exact.setScale(2, RoundingMode.UNNECESSARY)); // 2.35
// 下面这行会抛异常:ArithmeticException
// value.setScale(2, RoundingMode.UNNECESSARY);
}
}
/**
* 推荐使用场景:
*
* 1. 一般商业计算:RoundingMode.HALF_UP(四舍五入)⭐⭐⭐⭐⭐
* 2. 金融计算:RoundingMode.HALF_EVEN(银行家舍入)⭐⭐⭐⭐
* 3. 库存扣减:RoundingMode.DOWN(向下取整,宁少勿多)⭐⭐⭐
* 4. 税费计算:RoundingMode.UP(向上取整)⭐⭐⭐
*/
💾 方案2:分为单位(整数运算)
/**
* 分为单位方案
*
* 核心思想:将金额转换为分(最小单位),使用整数运算,避免浮点数精度问题
*/
public class CentCalculation {
/**
* 元转分
*/
public static long yuan2cent(BigDecimal yuan) {
if (yuan == null) {
return 0L;
}
// 乘以100,转换为分
return yuan.multiply(new BigDecimal("100"))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
}
/**
* 分转元
*/
public static BigDecimal cent2yuan(long cent) {
return new BigDecimal(cent)
.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
}
/**
* 示例:订单金额计算
*/
public static void main(String[] args) {
// 商品价格:99.99元
BigDecimal price = new BigDecimal("99.99");
// 数量:3
int quantity = 3;
// 方式1:BigDecimal计算
BigDecimal total1 = price.multiply(new BigDecimal(quantity))
.setScale(2, RoundingMode.HALF_UP);
System.out.println("BigDecimal计算:" + total1); // 299.97
// 方式2:分为单位计算
long priceCent = yuan2cent(price); // 9999分
long totalCent = priceCent * quantity; // 29997分
BigDecimal total2 = cent2yuan(totalCent); // 299.97元
System.out.println("分为单位计算:" + total2); // 299.97
}
}
/**
* 优点:
* ✅ 性能好(整数运算比BigDecimal快)
* ✅ 避免浮点数精度问题
* ✅ 数据库可以用BIGINT存储
*
* 缺点:
* ⚠️ 需要来回转换
* ⚠️ 代码可读性稍差
* ⚠️ 不适合需要高精度的场景(如汇率计算)
*
* 适用场景:
* ✅ 高性能要求
* ✅ 只需要精确到分
* ✅ 电商订单、支付系统
*/
💾 数据库存储方案
-- ❌ 错误的字段类型
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
total_amount FLOAT, -- ❌ 绝对不要用FLOAT/DOUBLE
discount_amount DOUBLE -- ❌ 精度丢失
);
-- ✅ 正确的字段类型
CREATE TABLE `order` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
-- 方案1:DECIMAL(推荐)⭐⭐⭐⭐⭐
total_amount DECIMAL(10, 2) NOT NULL COMMENT '订单总金额(元)',
discount_amount DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '优惠金额(元)',
freight_amount DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '运费(元)',
actual_amount DECIMAL(10, 2) NOT NULL COMMENT '实付金额(元)',
-- 方案2:BIGINT(分为单位)⭐⭐⭐⭐
total_amount_cent BIGINT NOT NULL COMMENT '订单总金额(分)',
discount_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '优惠金额(分)',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
/**
* DECIMAL类型说明:
* DECIMAL(M, D)
* - M:总位数(整数位+小数位),最大65
* - D:小数位数,最大30
*
* 示例:
* DECIMAL(10, 2):最大值 99999999.99(8位整数,2位小数)
* DECIMAL(15, 2):最大值 9999999999999.99(13位整数,2位小数)
*
* 推荐:
* - 普通金额:DECIMAL(10, 2)
* - 大额金额:DECIMAL(15, 2)
* - 汇率等:DECIMAL(10, 6)
*/
🎯 实战案例:订单金额计算
/**
* 订单金额计算服务
*/
@Service
@Slf4j
public class OrderAmountService {
/**
* 计算订单金额
*/
public OrderAmountDTO calculateOrderAmount(OrderCalculateDTO dto) {
OrderAmountDTO result = new OrderAmountDTO();
// 1. 计算商品总金额
BigDecimal goodsAmount = calculateGoodsAmount(dto.getItems());
result.setGoodsAmount(goodsAmount);
// 2. 计算运费
BigDecimal freightAmount = calculateFreight(dto);
result.setFreightAmount(freightAmount);
// 3. 计算优惠金额
BigDecimal discountAmount = calculateDiscount(dto, goodsAmount);
result.setDiscountAmount(discountAmount);
// 4. ⚡ 计算实付金额(商品总额 + 运费 - 优惠)
BigDecimal actualAmount = goodsAmount
.add(freightAmount)
.subtract(discountAmount);
// 确保实付金额不为负数
if (MoneyUtils.lessThan(actualAmount, BigDecimal.ZERO)) {
actualAmount = BigDecimal.ZERO;
}
// 保留2位小数
actualAmount = MoneyUtils.setScale(actualAmount);
result.setActualAmount(actualAmount);
log.info("订单金额计算完成:商品{}元 + 运费{}元 - 优惠{}元 = 实付{}元",
goodsAmount, freightAmount, discountAmount, actualAmount);
return result;
}
/**
* 计算商品总金额
*/
private BigDecimal calculateGoodsAmount(List<OrderItemDTO> items) {
if (items == null || items.isEmpty()) {
return BigDecimal.ZERO;
}
return items.stream()
.map(item -> {
BigDecimal price = item.getPrice();
BigDecimal quantity = new BigDecimal(item.getQuantity());
// 单品金额 = 单价 * 数量
return MoneyUtils.multiply(price, quantity);
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* 计算运费
*/
private BigDecimal calculateFreight(OrderCalculateDTO dto) {
BigDecimal freightAmount = dto.getFreightAmount();
if (freightAmount == null) {
freightAmount = BigDecimal.ZERO;
}
// 满99包邮
BigDecimal goodsAmount = calculateGoodsAmount(dto.getItems());
BigDecimal freeFreightThreshold = new BigDecimal("99.00");
if (MoneyUtils.greaterThanOrEqual(goodsAmount, freeFreightThreshold)) {
freightAmount = BigDecimal.ZERO;
}
return freightAmount;
}
/**
* 计算优惠金额
*/
private BigDecimal calculateDiscount(OrderCalculateDTO dto, BigDecimal goodsAmount) {
BigDecimal totalDiscount = BigDecimal.ZERO;
// 1. 优惠券优惠
if (dto.getCouponId() != null) {
BigDecimal couponDiscount = calculateCouponDiscount(dto.getCouponId(), goodsAmount);
totalDiscount = totalDiscount.add(couponDiscount);
}
// 2. 满减优惠
BigDecimal fullReductionDiscount = calculateFullReduction(goodsAmount);
totalDiscount = totalDiscount.add(fullReductionDiscount);
// 3. 确保优惠金额不超过商品总额
if (MoneyUtils.greaterThan(totalDiscount, goodsAmount)) {
totalDiscount = goodsAmount;
}
return MoneyUtils.setScale(totalDiscount);
}
/**
* 计算优惠券优惠
*/
private BigDecimal calculateCouponDiscount(Long couponId, BigDecimal goodsAmount) {
// TODO: 查询优惠券信息
// 示例:8.8折优惠券
BigDecimal discount = new BigDecimal("8.8");
// 折后金额
BigDecimal discountAmount = MoneyUtils.discount(goodsAmount, discount);
// 优惠金额 = 原价 - 折后价
return MoneyUtils.subtract(goodsAmount, discountAmount);
}
/**
* 计算满减优惠
*/
private BigDecimal calculateFullReduction(BigDecimal goodsAmount) {
// 满200减30
BigDecimal threshold = new BigDecimal("200.00");
BigDecimal reduction = new BigDecimal("30.00");
if (MoneyUtils.greaterThanOrEqual(goodsAmount, threshold)) {
return reduction;
}
return BigDecimal.ZERO;
}
}
/**
* 订单金额DTO
*/
@Data
public class OrderAmountDTO {
/**
* 商品总金额
*/
private BigDecimal goodsAmount;
/**
* 运费
*/
private BigDecimal freightAmount;
/**
* 优惠金额
*/
private BigDecimal discountAmount;
/**
* 实付金额
*/
private BigDecimal actualAmount;
}
⚡ JSON序列化处理
/**
* BigDecimal的JSON序列化配置
*/
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// ⚡ BigDecimal序列化为字符串(避免精度丢失)
builder.serializerByType(BigDecimal.class, new JsonSerializer<BigDecimal>() {
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
} else {
// 保留2位小数,并转为字符串
gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toString());
}
}
});
// ⚡ 字符串反序列化为BigDecimal
builder.deserializerByType(BigDecimal.class, new JsonDeserializer<BigDecimal>() {
@Override
public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
String value = p.getText();
if (StringUtils.isBlank(value)) {
return BigDecimal.ZERO;
}
return new BigDecimal(value);
}
});
};
}
}
/**
* 或者在字段上使用注解
*/
@Data
public class OrderVO {
private Long id;
/**
* ⚡ 使用@JsonSerialize注解
*/
@JsonSerialize(using = ToStringSerializer.class)
private BigDecimal totalAmount;
/**
* ⚡ 或者自定义格式化
*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#0.00")
private BigDecimal actualAmount;
}
✅ 最佳实践
金额计算黄金法则:
1️⃣ 数据类型:
□ Java:BigDecimal(禁止Float/Double)
□ 数据库:DECIMAL(M, D)
□ 接口传输:String(JSON)
2️⃣ 创建BigDecimal:
□ ✅ new BigDecimal("100.00") // 字符串
□ ✅ BigDecimal.valueOf(100.00)
□ ❌ new BigDecimal(0.1) // double(精度丢失)
3️⃣ 舍入模式:
□ 一般计算:RoundingMode.HALF_UP(四舍五入)
□ 金融计算:RoundingMode.HALF_EVEN(银行家舍入)
□ 库存扣减:RoundingMode.DOWN(向下取整)
4️⃣ 比较判断:
□ ✅ amount.compareTo(other) == 0 // 相等
□ ❌ amount.equals(other) // 不推荐(精度差异)
5️⃣ 格式化输出:
□ 保留2位小数
□ 千分位分隔
□ 货币符号
6️⃣ 性能优化:
□ 复用BigDecimal对象
□ 使用分为单位(高性能场景)
□ 缓存常用值(0、1、100等)
7️⃣ 安全防护:
□ 非空校验
□ 精度控制
□ 范围校验
□ 日志记录
🎉 总结
金额计算核心要点:
1️⃣ 禁止使用Float/Double
2️⃣ 使用BigDecimal或分为单位
3️⃣ 注意舍入模式(HALF_UP)
4️⃣ 用compareTo而不是equals
5️⃣ 数据库用DECIMAL类型
6️⃣ JSON传输用字符串
7️⃣ 充分测试边界情况
记住:金额计算无小事,一分一厘都要精准! 💰
文档编写时间:2025年10月24日
作者:热爱精确计算的金融工程师
版本:v1.0
愿每一笔账都清清楚楚! ✨