Java 的金额计算用 long 还是 BigDecimal?资深程序员这样选

前言

最近接触一个新项目,发现系统中所有金额相关字段都使用long类型来表示。

作为一个习惯使用BigDecimal处理金额的开发者,这让我产生了疑惑:这会不会有精度问题?为什么要这样设计?

"用double不行吗?它也能表示小数啊!"

经过一番研究和思考,把最终得出的结论来和大家详细分享一下。

一、double、float类型比较

doublefloat都是Java中的浮点数类型,它们采用IEEE 754标准来表示小数。

这种表示方法类似于科学计数法,但存在一个致命问题:不是所有小数都能精确表示

举一个简单的例子:

java 复制代码
double a = 0.1;
double b = 0.2;
double result = a + b;
System.out.println(result); 
// 输出:0.30000000000000004

什么?简单的0.1 + 0.2居然不等于0.3?

是的,这是因为在二进制的计算中,有些十进制小数没有办法精确表示,就像1除以3一样,在十进制中也不能精确表示(0.33333...)。

二、BigDecimal 解决方案

BigDecimal的正确用法

BigDecimal可以解决精度问题,但必须正确使用。

错误的构造方式

java 复制代码
BigDecimal wrong1 = new BigDecimal(0.1); 
BigDecimal wrong2 = new BigDecimal(0.2);

这种传入浮点型的构造方式还是会有精度问题。

正确的构造方式

java 复制代码
BigDecimal correct1 = new BigDecimal("0.1");
BigDecimal correct2 = new BigDecimal("0.2");
System.out.println("正确方式: " + correct1.add(correct2)); // 0.3

通过传入字符串进行构造。

或者用valueOf(内部也是转字符串)

java 复制代码
BigDecimal safe1 = BigDecimal.valueOf(0.1);
BigDecimal safe2 = BigDecimal.valueOf(0.2);
System.out.println("安全方式: " + safe1.add(safe2)); // 0.3

BigDecimal的运算规则

java 复制代码
public class BigDecimalOperations {
    public static void main(String[] args) {
        BigDecimal price = new BigDecimal("19.99");
        BigDecimal quantity = new BigDecimal("3");
        BigDecimal taxRate = new BigDecimal("0.13");
        
        // 乘法
        BigDecimal subtotal = price.multiply(quantity);
        
        // 除法必须指定精度和舍入模式
        BigDecimal tax = subtotal.multiply(taxRate)
                               .setScale(2, RoundingMode.HALF_UP);
        
        // 加法
        BigDecimal total = subtotal.add(tax);
        
        System.out.println("小计: " + subtotal); // 59.97
        System.out.println("税金: " + tax);      // 7.80
        System.out.println("总计: " + total);    // 67.77
    }
}

BigDecimal的比较操作

java 复制代码
public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("10.00");
        BigDecimal num2 = new BigDecimal("10.000");
        
        // 错误:比较引用
        System.out.println("== 比较: " + (num1 == num2)); // false
        
        // 错误:equals会比较精度
        System.out.println("equals比较: " + num1.equals(num2)); // false
        
        // 正确:compareTo只比较数值
        System.out.println("compareTo比较: " + (num1.compareTo(num2) == 0)); // true
    }
}

BigDecimal的缺点

  1. 性能开销:对象创建和运算成本高
  2. 内存占用:每个对象需要20-30字节
  3. 使用复杂:容易用错构造方法和运算规则
  4. 代码冗长:简单的运算也需要多行代码

三、long方案

核心思想:以分为单位存储

使用long表示金额的核心思路是:不以元为单位,而以最小货币单位(分)存储。

java 复制代码
public class LongAmountDemo {
    // 工具方法:元转分
    public static long yuanToFen(double yuan) {
        return Math.round(yuan * 100);
    }
    
    // 工具方法:分转元(用于显示)
    public static String fenToYuanDisplay(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
    
    // 工具方法:分转元(用于计算)
    public static double fenToYuan(long fen) {
        return fen / 100.0;
    }
    
    public static void main(String[] args) {
        // 以分为单位存储所有金额
        long price = yuanToFen(19.99);  // 1999分 = 19.99元
        long quantity = 3;
        long taxRate = 13;  // 13% = 0.13,这里用整数表示百分比
        
        // 计算过程全部使用整数运算
        long subtotal = price * quantity;                    // 5997分
        long tax = (subtotal * taxRate) / 100;               // 780分
        long total = subtotal + tax;                         // 6777分
        
        System.out.println("小计: " + fenToYuanDisplay(subtotal)); // ¥59.97
        System.out.println("税金: " + fenToYuanDisplay(tax));      // ¥7.80
        System.out.println("总计: " + fenToYuanDisplay(total));    // ¥67.77
    }
}

long方案的优势

1.绝对精确 整数运算在计算机中是精确的,不会出现浮点数的精度问题。

2.性能卓越

java 复制代码
// long运算 - 机器指令级别,极快
long a = 1000L, b = 2000L;
long result = a + b;

// BigDecimal运算 - 方法调用,对象创建,较慢
BigDecimal c = new BigDecimal("10.00");
BigDecimal d = new BigDecimal("20.00");
BigDecimal result2 = c.add(d);

3.存储高效

  • long:固定8字节
  • BigDecimal:对象头+数值,通常20-30字节

4.序列化简单 在数据库、JSON、网络传输中处理更简单。

实际业务应用

java 复制代码
public class OrderService {
    // 订单金额(分)
    private long orderAmount;
    // 优惠金额(分)
    private long discountAmount;
    // 实付金额(分)
    private long actualAmount;
    
    public void calculateOrder(long unitPrice, int quantity, long discountRate) {
        // 计算订单金额
        orderAmount = unitPrice * quantity;
        
        // 计算优惠金额
        discountAmount = (orderAmount * discountRate) / 100;
        
        // 计算实付金额
        actualAmount = orderAmount - discountAmount;
    }
    
    // 显示方法
    public String getDisplayAmount() {
        return String.format("订单金额: %s, 优惠: %s, 实付: %s", 
            formatFen(orderAmount),
            formatFen(discountAmount),
            formatFen(actualAmount));
    }
    
    private String formatFen(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
}

四、各种方案的适用场景

double/float:绝对不要用于金额

  • 科学计算、图形处理
  • 统计分析(允许误差)
  • 物理模拟
  • 不适用于任何金额计算

BigDecimal:复杂金融计算

  • 高精度利率、汇率计算
  • 税务计算(需要复杂小数运算)
  • 银行核心系统
  • 需要任意精度的场景

long:大多数业务系统

  • 电商订单系统
  • 支付系统
  • 账户余额管理
  • 积分、优惠券系统

五、BigDecimal的最佳实践

如果确实需要使用BigDecimal,请遵循以下规则:

1. 构造方法

java 复制代码
// 推荐
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.1);  // 内部转字符串

// 避免
BigDecimal c = new BigDecimal(0.1);      // 精度问题

2. 运算控制

java 复制代码
BigDecimal num1 = new BigDecimal("10.00");
BigDecimal num2 = new BigDecimal("3.00");

// 除法必须指定精度
BigDecimal result = num1.divide(num2, 4, RoundingMode.HALF_UP);

// 乘法建议控制精度
BigDecimal product = num1.multiply(num2).setScale(2, RoundingMode.HALF_UP);

3. 数值比较

java 复制代码
BigDecimal amount1 = new BigDecimal("100.00");
BigDecimal amount2 = new BigDecimal("100.000");

// 正确
if (amount1.compareTo(amount2) == 0) {
    // 数值相等
}

// 错误
if (amount1.equals(amount2)) {
    // 不会执行,因为精度不同
}

六、long方案的实施建议

1. 建立工具类

java 复制代码
public class MoneyUtils {
    private MoneyUtils() {} // 工具类,防止实例化
    
    // 元转分
    public static long yuanToFen(double yuan) {
        return Math.round(yuan * 100);
    }
    
    // 分转元(显示用)
    public static String fenToDisplayYuan(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
    
    // 分转元(计算用)
    public static double fenToYuan(long fen) {
        return fen / 100.0;
    }
    
    // 金额加法(防止溢出)
    public static long add(long amount1, long amount2) {
        return Math.addExact(amount1, amount2);
    }
    
    // 金额乘法
    public static long multiply(long amount, int multiplier) {
        return Math.multiplyExact(amount, multiplier);
    }
}

2. 数据库设计

sql 复制代码
-- 使用bigint存储金额(分)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_amount BIGINT COMMENT '订单金额(分)',
    discount_amount BIGINT COMMENT '优惠金额(分)',
    actual_amount BIGINT COMMENT '实付金额(分)'
);

3. API设计

java 复制代码
// 请求和响应中使用Long类型表示金额(分)
public class OrderRequest {
    private Long productId;
    private Integer quantity;
    private Long unitPrice;  // 单价(分)
}

public class OrderResponse {
    private String orderNo;
    private Long totalAmount;  // 总金额(分)
    private String displayAmount; // 显示金额 "¥99.99"
}

总结与选择

为什么那个项目选择long?

现在我可以理解那个项目的设计思路了:

  1. 业务特性:主要是简单的加减乘运算,没有复杂的小数除法
  2. 性能要求:高并发场景,需要最优性能
  3. 团队协作:统一规范,避免BigDecimal的误用
  4. 系统复杂度:降低系统复杂度和维护成本

如何选择?

场景 推荐方案 理由
简单业务系统 long 性能好,简单可靠
银行金融系统 BigDecimal 精度要求极高
高并发交易 long 性能至关重要
复杂税务计算 BigDecimal 需要复杂小数运算
新项目启动 long 技术债务少

技术选型从来都不是单一的,理解业务的需求,能选择出最适合的技术方案就是最好的方案。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《async/await 到底要不要加 try-catch?异步错误处理最佳实践》

《Vue3 和 Vue2 的核心区别?很多开发者都没完全搞懂的 10 个细节》

《Java 开发必看:什么时候用 for,什么时候用 Stream?》

《这 10 个 MySQL 高级用法,让你的代码又快又好看》

相关推荐
Coder_Boy_2 小时前
业务导向型技术日志记录(2)
java·人工智能·驱动开发·微服务
凤凰战士芭比Q2 小时前
Jenkins(环境变量、构建参数、流水线触发、通知报告)
java·servlet·jenkins
运维@小兵2 小时前
Spring AI系列——开发MCP Server和MCP Client(SSE方式)
java·人工智能·spring
有一个好名字2 小时前
设计模式-代理模式
java·设计模式·代理模式
IT 行者2 小时前
Spring Security 7.0 迁移指南
java·数据库·spring
12344522 小时前
【MCP入门篇】从0到1教你搭建MCP服务
后端·mcp
okseekw2 小时前
Java多线程开发实战:解锁线程安全与性能优化的关键技术
java·后端
HuangYongbiao2 小时前
NestJS 架构设计系列:应用服务与领域服务的区别
后端·架构
技术不打烊2 小时前
MySQL主从延迟飙升?元数据锁可能是“真凶”
后端