前言
最近接触一个新项目,发现系统中所有金额相关字段都使用long类型来表示。
作为一个习惯使用BigDecimal处理金额的开发者,这让我产生了疑惑:这会不会有精度问题?为什么要这样设计?
"用double不行吗?它也能表示小数啊!"
经过一番研究和思考,把最终得出的结论来和大家详细分享一下。
一、double、float类型比较
double和float都是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的缺点
- 性能开销:对象创建和运算成本高
- 内存占用:每个对象需要20-30字节
- 使用复杂:容易用错构造方法和运算规则
- 代码冗长:简单的运算也需要多行代码
三、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?
现在我可以理解那个项目的设计思路了:
- 业务特性:主要是简单的加减乘运算,没有复杂的小数除法
- 性能要求:高并发场景,需要最优性能
- 团队协作:统一规范,避免BigDecimal的误用
- 系统复杂度:降低系统复杂度和维护成本
如何选择?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单业务系统 | long | 性能好,简单可靠 |
| 银行金融系统 | BigDecimal | 精度要求极高 |
| 高并发交易 | long | 性能至关重要 |
| 复杂税务计算 | BigDecimal | 需要复杂小数运算 |
| 新项目启动 | long | 技术债务少 |
技术选型从来都不是单一的,理解业务的需求,能选择出最适合的技术方案就是最好的方案。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《async/await 到底要不要加 try-catch?异步错误处理最佳实践》
《Vue3 和 Vue2 的核心区别?很多开发者都没完全搞懂的 10 个细节》