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 高级用法,让你的代码又快又好看》

相关推荐
栈与堆6 分钟前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥14 分钟前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似19 分钟前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole
马卡巴卡20 分钟前
Java CompletableFuture 接口与原理详解
后端
OC溥哥99921 分钟前
Paper MinecraftV3.0重大更新(下界更新)我的世界C++2D版本隆重推出,拷贝即玩!
java·c++·算法
星火开发设计24 分钟前
C++ map 全面解析与实战指南
java·数据结构·c++·学习·算法·map·知识
*才华有限公司*27 分钟前
RTSP视频流播放系统
java·git·websocket·网络协议·信息与通信
神奇小汤圆28 分钟前
Java线程协作工具:CountDownLatch 、CyclicBarrier、Phaser、Semaphore 、Exchanger
后端
gelald38 分钟前
ReentrantLock 学习笔记
java·后端
计算机学姐1 小时前
基于SpringBoot的校园资源共享系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·spring·信息可视化