找回密码
 立即注册
首页 业界区 业界 金额计算字段类型用Long,还是BigDecimal更好? ...

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

裴竹悦 2026-1-18 16:25:03
前言

对于从事后端开发的小伙伴来说,可能会遇到金额计算字段的类型,到底该用Long,还是BigDecimal的困扰。
甚至有些公司的架构师跟DBA,有时也会为了金额计算字段的类型而PK。
今天这篇文章专门跟大家一起聊聊这个话题,希望对你会有所帮助。
一、案发现场

有些小伙伴在工作中可能遇到过这样的场景:新来的开发小明负责公司电商平台的优惠券计算功能。
按照产品需求,满100减20的优惠券,用户下单金额是98.5元时,应该无法使用这张优惠券。
小明心想:这太简单了!
不到5分钟就写完了代码:
  1. 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 二进制世界的局限
  1. 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));    }}
复制代码
运行结果会让你震惊:
  1. 0.1 + 0.2 = 0.300000000000000040.1 + 0.2 == 0.3 ? false0.1的实际值: 0.10000000000000000555111512312578270211815834045410156250.2的实际值: 0.2000000000000000111022302462515654042363166809082031250.1+0.2的实际值: 0.3000000000000000444089209850062616169452667236328125
复制代码
2.2 为什么会出现精度问题?

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

如何出现的问题?

这就好比用1/3 ≈ 0.333333来表示三分之一,永远无法精确。
计算机的二进制系统也无法精确表示某些十进制小数。
三、两种解决方案的深度PK

面对金额计算的精度问题,Java开发者主要有两种选择。
让我们深入剖析每种方案的实现和原理。
3.1 方案一:货币使用Long

这种方法的核心思想:用分来计算,不用元
  1. 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;    }}
复制代码
实战场景
  1. 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提供的专门用于精确计算的类。
  1. 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的陷阱与正确用法
  1. 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 性能基准测试
  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));    }}
复制代码
典型测试结果:
  1. Long方案耗时: 25msBigDecimal方案耗时: 1250ms性能差异倍数: 50.0
复制代码
性能差距可达数十倍!这是为什么呢?
4.2 存储结构与原理分析

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



4.3 数据库层面的考虑
  1. -- 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)  -- 索引相对较大);
复制代码
数据库层面的差异

  • 存储空间:BIGINT固定8字节,DECIMAL是变长的
  • 索引效率:BIGINT比较更快
  • 跨数据库兼容性:BIGINT几乎所有数据库都支持且行为一致
  • 计算位置:DECIMAL可以在数据库层计算,但业务逻辑通常应在应用层
五、真实业务场景深度分析

没有银弹,只有适合场景的方案
5.1 场景一:金融交易系统(推荐Long)
  1. // 银行核心系统示例public class BankTransactionSystem {    // 账户余额(单位:分)    private AtomicLong balanceInCents = new AtomicLong();        // 存款(线程安全)    public boolean deposit(long cents) {        if (cents = 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 allocateDiscount(        Map itemPrices,        BigDecimal totalDiscount    ) {        BigDecimal totalPrice = itemPrices.values().stream()            .reduce(BigDecimal.ZERO, BigDecimal::add);                Map result = new HashMap();        BigDecimal allocated = BigDecimal.ZERO;                List 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 混合方案:鱼与熊掌兼得

有些复杂的系统会采用混合方案:
  1. 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:序列化问题
  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:等于判断的坑
  1. 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:溢出问题
  1. 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 代码规范建议
  1. // 金额处理的工具类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 终极建议


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

    • 支付、清算、账户余额等
    • 理由:性能、原子性、一致性

  • 电商营销系统:优先使用BigDecimal方案

    • 优惠计算、价格引擎、促销活动
    • 理由:灵活性、计算精度、业务变化快

  • 混合型系统:采用分层架构

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

7.3 最后的建议

记住这三条铁律

  • 金额计算无小事,必须严格测试
  • 选择适合业务的技术,而不是最新的技术
  • 保持一致性,一个系统内不要混用多种方案
技术选型就像选工具,用对了事半功倍,用错了后患无穷。
希望这篇文章能帮你在金额计算的路上少踩坑,走得更稳更远。
更多项目实战在我的技术网站:http://www.susan.net.cn/project
最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册