news 2026/3/26 9:48:40

金额计算字段类型用Long,还是BigDecimal更好?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
金额计算字段类型用Long,还是BigDecimal更好?

前言

对于从事后端开发的小伙伴来说,可能会遇到金额计算字段的类型,到底该用Long,还是BigDecimal的困扰。

甚至有些公司的架构师跟DBA,有时也会为了金额计算字段的类型而PK。

今天这篇文章专门跟大家一起聊聊这个话题,希望对你会有所帮助。

一、案发现场

有些小伙伴在工作中可能遇到过这样的场景:新来的开发小明负责公司电商平台的优惠券计算功能。

按照产品需求,满100减20的优惠券,用户下单金额是98.5元时,应该无法使用这张优惠券。

小明心想:这太简单了!

不到5分钟就写完了代码:

/* by 01022.hk - online tools website : 01022.hk/zh/linuxcmd.html */ 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,为什么条件判断会出错呢?

二、浮点数的陷阱:计算机的小秘密

要理解这个问题,我们需要知道计算机是如何存储小数的。

2.1 二进制世界的局限

/* by 01022.hk - online tools website : 01022.hk/zh/linuxcmd.html */ 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

2.2 为什么会出现精度问题?

用一张图来理解浮点数的存储原理:

如何出现的问题?

这就好比用1/3 ≈ 0.333333来表示三分之一,永远无法精确。

计算机的二进制系统也无法精确表示某些十进制小数。

三、两种解决方案的深度PK

面对金额计算的精度问题,Java开发者主要有两种选择。

让我们深入剖析每种方案的实现和原理。

3.1 方案一:货币使用Long

这种方法的核心思想:用分来计算,不用元

public class MoneyWithLong { // 所有金额都以分为单位存储 private Long amountInCents; public MoneyWithLong(Long amountInCents) { this.amountInCents = amountInCents; } // 加法 public MoneyWithLong add(MoneyWithLong other) { return new MoneyWithLong(this.amountInCents + other.amountInCents); } // 减法 public MoneyWithLong subtract(MoneyWithLong other) { return new MoneyWithLong(this.amountInCents - other.amountInCents); } // 乘法(处理折扣等场景) public MoneyWithLong multiply(double multiplier) { // 先将double转为整数分计算 BigDecimal bd = BigDecimal.valueOf(multiplier) .multiply(BigDecimal.valueOf(this.amountInCents)); return new 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元 } }

3.2 方案二:BigDecimal精确计算

BigDecimal是Java提供的专门用于精确计算的类。

public class MoneyWithBigDecimal { private BigDecimal amount; private static final int SCALE = 2; // 保留2位小数 private static final 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) { return new MoneyWithBigDecimal(this.amount.add(other.amount)); } public MoneyWithBigDecimal subtract(MoneyWithBigDecimal other) { return new MoneyWithBigDecimal(this.amount.subtract(other.amount)); } public MoneyWithBigDecimal multiply(BigDecimal multiplier) { return new MoneyWithBigDecimal( this.amount.multiply(multiplier).setScale(SCALE, ROUNDING_MODE) ); } public MoneyWithBigDecimal divide(BigDecimal divisor) { return new MoneyWithBigDecimal( this.amount.divide(divisor, SCALE, ROUNDING_MODE) ); } // 比较 public int compareTo(MoneyWithBigDecimal other) { return this.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 } }

四、性能与存储的深度对比

有些小伙伴在工作中可能会问:两种方案性能差别大吗?对数据库有什么影响?

4.1 性能基准测试

public class PerformanceBenchmark { private static final int 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

性能差距可达数十倍!这是为什么呢?

4.2 存储结构与原理分析

下面用几张图对比两种方案的存储:

4.3 数据库层面的考虑

-- Long方案对应的表结构 CREATE TABLE orders_long ( id BIGINT PRIMARY KEY, amount_cents BIGINT NOT NULL, -- 以分为单位 INDEX idx_amount (amount_cents) -- 索引效率高 ); -- BigDecimal方案对应的表结构 CREATE TABLE orders_bd ( id BIGINT PRIMARY KEY, amount DECIMAL(20, 2) NOT NULL, -- 总共20位,2位小数 INDEX idx_amount (amount) -- 索引相对较大 );

数据库层面的差异

  1. 存储空间:BIGINT固定8字节,DECIMAL是变长的
  2. 索引效率:BIGINT比较更快
  3. 跨数据库兼容性:BIGINT几乎所有数据库都支持且行为一致
  4. 计算位置:DECIMAL可以在数据库层计算,但业务逻辑通常应在应用层

五、真实业务场景深度分析

没有银弹,只有适合场景的方案

5.1 场景一:金融交易系统(推荐Long)

// 银行核心系统示例 public class BankTransactionSystem { // 账户余额(单位:分) private AtomicLong balanceInCents = new AtomicLong(); // 存款(线程安全) public boolean deposit(long cents) { if (cents <= 0) return false; balanceInCents.addAndGet(cents); return true; } // 取款(防止超取) public boolean withdraw(long cents) { while (true) { long current = balanceInCents.get(); if (current < cents) return false; if (balanceInCents.compareAndSet(current, current - cents)) { return true; } // CAS失败,重试 } } // 跨行转账(两阶段提交) public boolean transfer(BankTransactionSystem target, long cents) { if (!this.withdraw(cents)) { return false; } try { if (!target.deposit(cents)) { // 存款失败,回滚 this.deposit(cents); return false; } return true; } catch (Exception e) { this.deposit(cents); // 异常回滚 throw e; } } }

为什么金融系统偏爱Long

  1. 原子性操作:Java对long的原子操作支持完善(AtomicLong)
  2. 高性能:每秒数万笔交易必须考虑性能
  3. 精确无误差:分是最小单位,没有舍入问题
  4. 审计方便:所有操作都是整数,便于对账

5.2 场景二:电商优惠计算(BigDecimal更灵活)

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; } }

5.3 混合方案:鱼与熊掌兼得

有些复杂的系统会采用混合方案:

public class HybridMoneySystem { // 核心账户系统用Long private static class AccountCore { private long balanceCents; // 分单位 public void transfer(AccountCore to, long cents) { // 高性能的整数运算 this.balanceCents -= cents; to.balanceCents += cents; } } // 营销计算用BigDecimal private static class 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) { return new BigDecimal(cents) .divide(new BigDecimal("100"), 2, RoundingMode.UNNECESSARY); } }

六、避坑指南

6.1 常见的坑

坑1:序列化问题

public class SerializationBug { // 使用默认序列化 private BigDecimal amount; // 正确做法 private transient 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"))); // 正确计算 } }

6.2 代码规范建议

// 金额处理的工具类 public final class MoneyUtils { private MoneyUtils() {} // 工具类私有构造 // 全局统一的精度和舍入模式 public static final int DEFAULT_SCALE = 2; public static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_UP; // 安全的创建方法 public static BigDecimal safeCreate(String amount) { try { return new BigDecimal(amount).setScale(DEFAULT_SCALE, DEFAULT_ROUNDING); } catch (NumberFormatException e) { throw new 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) return false; if (amount.scale() > DEFAULT_SCALE) return false; 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); } }

七、总结

文章最后跟大家总结一下。

7.1 选择原则

我画了一张图帮你做选择:

7.2 终极建议

  1. 金融核心系统:优先使用Long方案

    • 支付、清算、账户余额等
    • 理由:性能、原子性、一致性
  2. 电商营销系统:优先使用BigDecimal方案

    • 优惠计算、价格引擎、促销活动
    • 理由:灵活性、计算精度、业务变化快
  3. 混合型系统:采用分层架构

    • 核心层用Long保证性能
    • 计算层用BigDecimal保证精度
    • 表现层做好格式化显示

7.3 最后的建议

记住这三条铁律

  1. 金额计算无小事,必须严格测试
  2. 选择适合业务的技术,而不是最新的技术
  3. 保持一致性,一个系统内不要混用多种方案

技术选型就像选工具,用对了事半功倍,用错了后患无穷。

希望这篇文章能帮你在金额计算的路上少踩坑,走得更稳更远。

更多项目实战在我的技术网站:http://www.susan.net.cn/project

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/23 12:29:24

生活道理(不定期更新)

仅供参考。 1.刀在好人手里是厨具&#xff0c;在坏人手里是凶器&#xff0c;不要乱递刀&#xff0c;递刀前要看人品。 2.三次不回就不用联系了&#xff0c;朋友只存在于重视你的人之中。选人可以通过这个人身上发生的事情或者产生事情来看这个人。 3.说话的人要注意用别人能接受…

作者头像 李华
网站建设 2026/3/17 6:22:42

使用声波进行定位的技术综述

一、引言 在定位技术领域&#xff0c;GNSS&#xff08;GPS/北斗&#xff09;、Wi‑Fi、蓝牙、UWB 已成为主流方案。但在 室内、地下、隧道、水下、高干扰环境 等场景中&#xff0c;电磁波定位往往失效或精度急剧下降。声波&#xff08;Acoustic / Ultrasonic&#xff09;由于其…

作者头像 李华
网站建设 2026/3/14 22:02:15

使用 perf + FlameGraph 生成火焰图(Flame Graph)笔记

使用 perf FlameGraph 生成火焰图&#xff08;Flame Graph&#xff09;笔记使用 perf FlameGraph 生成火焰图&#xff08;Flame Graph&#xff09;笔记一、什么是火焰图&#xff08;Flame Graph&#xff09;火焰图的核心含义二、整体流程概览三、准备环境1️⃣ 安装 perf2️⃣…

作者头像 李华
网站建设 2026/3/21 12:56:49

基于单像素成像和深度学习的光学图像加密研究【附源码】

✅ 博主简介&#xff1a;擅长数据搜集与处理、建模仿真、程序设计、仿真代码、论文写作与指导&#xff0c;毕业论文、期刊论文经验交流。✅成品或者定制&#xff0c;扫描文章底部微信二维码。(1) 基于单像素成像与未训练神经网络的光学图像隐写术信息安全领域中&#xff0c;图像…

作者头像 李华
网站建设 2026/3/25 16:40:00

视觉SLAM位姿估计深度学习技术应用【附完整代码】

✅ 博主简介&#xff1a;擅长数据搜集与处理、建模仿真、程序设计、仿真代码、论文写作与指导&#xff0c;毕业论文、期刊论文经验交流。✅成品或者定制&#xff0c;扫描文章底部微信二维码。&#xff08;1&#xff09;基于多平面分割的位姿求解方法 在视觉SLAM系统中&#xff…

作者头像 李华
网站建设 2026/3/19 17:18:01

python基于Web技术的智能养老管理系统

目录基于Web技术的智能养老管理系统摘要开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;基于Web技术的智能养老管理系统摘要 随着人口老龄化加剧&#xff0c;传统养老模式面临资源分配不均、…

作者头像 李华