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

前言

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

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

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

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

一、double、float类型比较

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

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

举一个简单的例子:

ini 复制代码
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可以解决精度问题,但必须正确使用。

错误的构造方式

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

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

正确的构造方式

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

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

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

ini 复制代码
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的比较操作

csharp 复制代码
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表示金额的核心思路是:不以元为单位,而以最小货币单位(分)存储。

arduino 复制代码
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.性能卓越

ini 复制代码
// 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、网络传输中处理更简单。

实际业务应用

arduino 复制代码
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. 构造方法

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

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

2. 运算控制

ini 复制代码
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. 建立工具类

arduino 复制代码
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设计

kotlin 复制代码
// 请求和响应中使用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 技术债务少

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

相关推荐
程序员柒叔2 小时前
OpenClaw Agent 运行时模块分析
后端·github
咸鱼翻身了么2 小时前
大文件上传-spark-md5
前端·后端
共享家95272 小时前
C++ 日志类设计
linux·c++·后端
掘金者阿豪2 小时前
一次 AI 调用 15 万 Token 只花了 \$0.058?彻底搞懂 Token、缓存读、补全计费机制!(附完整架构图)
后端
程序员柒叔2 小时前
OpenClaw 踩坑记:Cron 任务 Feishu 推送失败
后端·github
AskHarries2 小时前
在 AI 快速发展的今天,“人还重要吗?
后端
SimonKing2 小时前
OpenCode 20 个斜杠命令,90% 的人只用过 3 个
java·后端·程序员
Gopher_HBo2 小时前
BlockingQueue详解
java·后端
米糕闯编程2 小时前
IDEA新建springboot项目
spring boot·后端·intellij-idea