对于从事后端开发的小伙伴来说,可能会遇到金额计算字段的类型,到底该用Long,还是BigDecimal的困扰。
甚至有些公司的架构师跟DBA,有时也会为了金额计算字段的类型而PK。
今天这篇文章专门跟大家一起聊聊这个话题,希望对你会有所帮助。
有些小伙伴在工作中可能遇到过这样的场景:新来的开发小明负责公司电商平台的优惠券计算功能。
按照产品需求,满100减20的优惠券,用户下单金额是98.5元时,应该无法使用这张优惠券。
小明心想:这太简单了!
不到5分钟就写完了代码:
public class CouponService {
public boolean canUseCoupon(double orderAmount, double couponThreshold) {
return orderAmount >= couponThreshold;
}
public static void main(String[] args) {
CouponService service = new CouponService();
double orderAmount = 98.5;
double couponThreshold = 100.0;
boolean canUse = service.canUseCoupon(orderAmount, couponThreshold);
System.out.println("订单金额" + orderAmount + "元,能否使用" +
couponThreshold + "元门槛优惠券:" + canUse);
// 输出:订单金额98.5元,能否使用100.0元门槛优惠券:true
}
}
结果上线第一天,财务就炸锅了:大量本不该享受优惠的订单都被系统通过了,一天下来公司损失了3万多元!
小明百思不得其解:98.5明明小于100,为什么条件判断会出错呢?
要理解这个问题,我们需要知道计算机是如何存储小数的。
public class FloatProblemDemo {
public static void main(String[] args) {
// 看似简单的计算,却有问题
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println("0.1 + 0.2 = " + c);
System.out.println("0.1 + 0.2 == 0.3 ? " + (c == 0.3));
// 让我们看看实际存储的值
System.out.println("0.1的实际值: " + new BigDecimal(a));
System.out.println("0.2的实际值: " + new BigDecimal(b));
System.out.println("0.1+0.2的实际值: " + new BigDecimal(c));
}
}
运行结果会让你震惊:
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 ? false
0.1的实际值: 0.1000000000000000055511151231257827021181583404541015625
0.2的实际值: 0.200000000000000011102230246251565404236316680908203125
0.1+0.2的实际值: 0.3000000000000000444089209850062616169452667236328125
用一张图来理解浮点数的存储原理:

如何出现的问题?

这就好比用1/3 ≈ 0.333333来表示三分之一,永远无法精确。
计算机的二进制系统也无法精确表示某些十进制小数。
面对金额计算的精度问题,Java开发者主要有两种选择。
让我们深入剖析每种方案的实现和原理。
这种方法的核心思想:用分来计算,不用元。
public class MoneyWithLong {
// 所有金额都以分为单位存储
private Long amountInCents;
public MoneyWithLong(Long amountInCents) {
this.amountInCents = amountInCents;
}
// 加法
public MoneyWithLong add(MoneyWithLong other) {
returnnew MoneyWithLong(this.amountInCents + other.amountInCents);
}
// 减法
public MoneyWithLong subtract(MoneyWithLong other) {
returnnew MoneyWithLong(this.amountInCents - other.amountInCents);
}
// 乘法(处理折扣等场景)
public MoneyWithLong multiply(double multiplier) {
// 先将double转为整数分计算
BigDecimal bd = BigDecimal.valueOf(multiplier)
.multiply(BigDecimal.valueOf(this.amountInCents));
returnnew MoneyWithLong(bd.longValue());
}
// 格式化显示
public String display() {
double yuan = amountInCents / 100.0;
return String.format("%.2f元", yuan);
}
// 小明问题的正确解法
public static boolean canUseCoupon(Long orderAmountInCents, Long thresholdInCents) {
return orderAmountInCents >= thresholdInCents;
}
}
实战场景:
public class LongSolutionDemo {
public static void main(String[] args) {
// 解决小明的问题
Long orderAmount = 9850L; // 98.50元
Long threshold = 10000L; // 100.00元
boolean canUse = orderAmount >= threshold;
System.out.println("订单98.5元能否使用100元门槛券: " + canUse);
// 正确输出:false
// 复杂计算示例
MoneyWithLong price1 = new MoneyWithLong(1999L); // 19.99元
MoneyWithLong price2 = new MoneyWithLong(2999L); // 29.99元
MoneyWithLong total = price1.add(price2);
System.out.println("总价: " + total.display()); // 49.98元
// 折扣计算
MoneyWithLong discounted = total.multiply(0.8); // 8折
System.out.println("8折后: " + discounted.display()); // 39.98元
}
}
BigDecimal是Java提供的专门用于精确计算的类。
public class MoneyWithBigDecimal {
private BigDecimal amount;
privatestaticfinalint SCALE = 2; // 保留2位小数
privatestaticfinal RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
public MoneyWithBigDecimal(String amount) {
this.amount = new BigDecimal(amount).setScale(SCALE, ROUNDING_MODE);
}
public MoneyWithBigDecimal(BigDecimal amount) {
this.amount = amount.setScale(SCALE, ROUNDING_MODE);
}
// 四则运算
public MoneyWithBigDecimal add(MoneyWithBigDecimal other) {
returnnew MoneyWithBigDecimal(this.amount.add(other.amount));
}
public MoneyWithBigDecimal subtract(MoneyWithBigDecimal other) {
returnnew MoneyWithBigDecimal(this.amount.subtract(other.amount));
}
public MoneyWithBigDecimal multiply(BigDecimal multiplier) {
returnnew MoneyWithBigDecimal(
this.amount.multiply(multiplier).setScale(SCALE, ROUNDING_MODE)
);
}
public MoneyWithBigDecimal divide(BigDecimal divisor) {
returnnew MoneyWithBigDecimal(
this.amount.divide(divisor, SCALE, ROUNDING_MODE)
);
}
// 比较
public int compareTo(MoneyWithBigDecimal other) {
returnthis.amount.compareTo(other.amount);
}
}
BigDecimal的陷阱与正确用法:
public class BigDecimalCorrectUsage {
public static void main(String[] args) {
// 错误用法:使用double构造
BigDecimal bad1 = new BigDecimal(0.1);
System.out.println("错误构造: " + bad1);
// 输出:0.1000000000000000055511151231257827021181583404541015625
// 正确用法1:使用String构造
BigDecimal good1 = new BigDecimal("0.1");
System.out.println("String构造: " + good1);
// 输出:0.1
//正确用法2:使用valueOf方法
BigDecimal good2 = BigDecimal.valueOf(0.1);
System.out.println("valueOf构造: " + good2);
// 输出:0.1
// 除法的坑
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
try {
// 不指定精度会抛异常
BigDecimal result = a.divide(b);
} catch (ArithmeticException e) {
System.out.println("必须指定精度: " + e.getMessage());
}
// 正确做法
BigDecimal correctResult = a.divide(b, 2, RoundingMode.HALF_UP);
System.out.println("10 ÷ 3 = " + correctResult); // 3.33
}
}
有些小伙伴在工作中可能会问:两种方案性能差别大吗?对数据库有什么影响?
public class PerformanceBenchmark {
privatestaticfinalint ITERATIONS = 10_000_000;
public static void main(String[] args) {
// Long方案性能
long longStart = System.currentTimeMillis();
long totalCents = 0L;
for (int i = 0; i < ITERATIONS; i++) {
totalCents += 100L; // 1元
totalCents -= 50L; // 0.5元
totalCents *= 2;
totalCents /= 2;
}
long longEnd = System.currentTimeMillis();
System.out.println("Long方案耗时: " + (longEnd - longStart) + "ms");
// BigDecimal方案性能
long bdStart = System.currentTimeMillis();
BigDecimal total = BigDecimal.ZERO;
for (int i = 0; i < ITERATIONS; i++) {
total = total.add(new BigDecimal("1.00"));
total = total.subtract(new BigDecimal("0.50"));
total = total.multiply(new BigDecimal("2"));
total = total.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
}
long bdEnd = System.currentTimeMillis();
System.out.println("BigDecimal方案耗时: " + (bdEnd - bdStart) + "ms");
System.out.println("性能差异倍数: " +
(bdEnd - bdStart) * 1.0 / (longEnd - longStart));
}
}
典型测试结果:
Long方案耗时: 25ms
BigDecimal方案耗时: 1250ms
性能差异倍数: 50.0
性能差距可达数十倍!这是为什么呢?
下面用几张图对比两种方案的存储:



-- Long方案对应的表结构
CREATETABLE orders_long (
idBIGINT PRIMARY KEY,
amount_cents BIGINTNOTNULL, -- 以分为单位
INDEX idx_amount (amount_cents) -- 索引效率高
);
-- BigDecimal方案对应的表结构
CREATETABLE orders_bd (
idBIGINT PRIMARY KEY,
amount DECIMAL(20, 2) NOTNULL, -- 总共20位,2位小数
INDEX idx_amount (amount) -- 索引相对较大
);
数据库层面的差异:
没有银弹,只有适合场景的方案。
// 银行核心系统示例
publicclass BankTransactionSystem {
// 账户余额(单位:分)
private AtomicLong balanceInCents = new AtomicLong();
// 存款(线程安全)
public boolean deposit(long cents) {
if (cents <= 0) returnfalse;
balanceInCents.addAndGet(cents);
returntrue;
}
// 取款(防止超取)
public boolean withdraw(long cents) {
while (true) {
long current = balanceInCents.get();
if (current < cents) returnfalse;
if (balanceInCents.compareAndSet(current, current - cents)) {
returntrue;
}
// CAS失败,重试
}
}
// 跨行转账(两阶段提交)
public boolean transfer(BankTransactionSystem target, long cents) {
if (!this.withdraw(cents)) {
returnfalse;
}
try {
if (!target.deposit(cents)) {
// 存款失败,回滚
this.deposit(cents);
returnfalse;
}
returntrue;
} catch (Exception e) {
this.deposit(cents); // 异常回滚
throw e;
}
}
}
为什么金融系统偏爱Long:
public class EcommercePriceEngine {
private BigDecimal price;
// 复杂优惠计算
public BigDecimal calculateFinalPrice(
BigDecimal originalPrice,
BigDecimal discountRate, // 折扣率
BigDecimal fullReduction, // 满减
BigDecimal coupon, // 优惠券
boolean isVIP // VIP折扣
) {
BigDecimal result = originalPrice;
// 折扣
if (discountRate != null) {
result = result.multiply(discountRate)
.setScale(2, RoundingMode.HALF_UP);
}
// 满减
if (fullReduction != null &&
result.compareTo(new BigDecimal("100")) >= 0) {
result = result.subtract(fullReduction);
}
// 优惠券
if (coupon != null) {
result = result.subtract(coupon).max(BigDecimal.ZERO);
}
// VIP额外95折
if (isVIP) {
result = result.multiply(new BigDecimal("0.95"))
.setScale(2, RoundingMode.HALF_UP);
}
return result;
}
// 分摊计算(如订单多个商品分摊优惠)
public Map<String, BigDecimal> allocateDiscount(
Map<String, BigDecimal> itemPrices,
BigDecimal totalDiscount
) {
BigDecimal totalPrice = itemPrices.values().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, BigDecimal> result = new HashMap<>();
BigDecimal allocated = BigDecimal.ZERO;
List<String> keys = new ArrayList<>(itemPrices.keySet());
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
BigDecimal price = itemPrices.get(key);
// 按比例分摊
BigDecimal ratio = price.divide(totalPrice, 10, RoundingMode.HALF_UP);
BigDecimal itemDiscount = totalDiscount.multiply(ratio)
.setScale(2, RoundingMode.HALF_UP);
// 最后一个商品承担剩余金额
if (i == keys.size() - 1) {
itemDiscount = totalDiscount.subtract(allocated);
}
result.put(key, price.subtract(itemDiscount));
allocated = allocated.add(itemDiscount);
}
return result;
}
}
有些复杂的系统会采用混合方案:
public class HybridMoneySystem {
// 核心账户系统用Long
privatestaticclass AccountCore {
privatelong balanceCents; // 分单位
public void transfer(AccountCore to, long cents) {
// 高性能的整数运算
this.balanceCents -= cents;
to.balanceCents += cents;
}
}
// 营销计算用BigDecimal
privatestaticclass MarketingCalculator {
public BigDecimal calculateCampaignEffect(
BigDecimal budget,
BigDecimal conversionRate,
BigDecimal avgOrderValue
) {
// 复杂的浮点计算
BigDecimal estimatedOrders = budget.multiply(conversionRate)
.divide(avgOrderValue, 4, RoundingMode.HALF_UP);
return estimatedOrders.setScale(0, RoundingMode.HALF_UP);
}
}
// 转换层
public static long yuanToCents(BigDecimal yuan) {
return yuan.multiply(new BigDecimal("100"))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
}
public static BigDecimal centsToYuan(long cents) {
returnnew BigDecimal(cents)
.divide(new BigDecimal("100"), 2, RoundingMode.UNNECESSARY);
}
}
坑1:序列化问题
public class SerializationBug {
// 使用默认序列化
private BigDecimal amount;
// 正确做法
privatetransient BigDecimal amount; // 不自动序列化
public String getAmountForJson() {
return amount.toString(); // 明确转为String
}
public void setAmountFromJson(String amountStr) {
this.amount = new BigDecimal(amountStr); // 明确从String构造
}
}
坑2:等于判断的坑
public class EqualityBug {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println("a.equals(b): " + a.equals(b)); // false!
System.out.println("a.compareTo(b): " + a.compareTo(b)); // 0
// BigDecimal的equals不仅比较值,还比较scale
System.out.println("a.scale(): " + a.scale()); // 1
System.out.println("b.scale(): " + b.scale()); // 2
}
}
坑3:溢出问题
public class OverflowBug {
public static void main(String[] args) {
// Long的溢出
long max = Long.MAX_VALUE;
System.out.println("MAX: " + max);
System.out.println("MAX + 1: " + (max + 1)); // 变成负数!
// BigDecimal没有溢出,但可能性能问题
BigDecimal huge = new BigDecimal(Long.MAX_VALUE);
System.out.println("BigDecimal MAX * 2: " +
huge.multiply(new BigDecimal("2"))); // 正确计算
}
}
// 金额处理的工具类
publicfinalclass MoneyUtils {
private MoneyUtils() {} // 工具类私有构造
// 全局统一的精度和舍入模式
publicstaticfinalint DEFAULT_SCALE = 2;
publicstaticfinal RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_UP;
// 安全的创建方法
public static BigDecimal safeCreate(String amount) {
try {
returnnew BigDecimal(amount).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
} catch (NumberFormatException e) {
thrownew IllegalArgumentException("无效金额: " + amount, e);
}
}
// 转换方法
public static long yuanToCents(BigDecimal yuan) {
return yuan.multiply(new BigDecimal("100"))
.setScale(0, DEFAULT_ROUNDING)
.longValueExact(); // 精确转换,溢出抛异常
}
// 验证方法
public static boolean isValidAmount(BigDecimal amount) {
if (amount == null) returnfalse;
if (amount.scale() > DEFAULT_SCALE) returnfalse;
return amount.compareTo(BigDecimal.ZERO) >= 0;
}
// 格式化显示
public static String format(BigDecimal amount) {
return String.format("¥%.2f", amount);
}
public static String format(long cents) {
return String.format("¥%.2f", cents / 100.0);
}
}
文章最后跟大家总结一下。
我画了一张图帮你做选择:

记住这三条铁律:
技术选型就像选工具,用对了事半功倍,用错了后患无穷。
希望这篇文章能帮你在金额计算的路上少踩坑,走得更稳更远。
此外,还包含13大技术专栏:系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等。