一文淺析Java金額計(jì)算用long還是BigDecimal
前言
最近接觸一個(gè)新項(xiàng)目,發(fā)現(xiàn)系統(tǒng)中所有金額相關(guān)字段都使用long類型來表示。
作為一個(gè)習(xí)慣使用BigDecimal處理金額的開發(fā)者,這讓我產(chǎn)生了疑惑:這會(huì)不會(huì)有精度問題?為什么要這樣設(shè)計(jì)?
“用double不行嗎?它也能表示小數(shù)?。?rdquo;
經(jīng)過一番研究和思考,把最終得出的結(jié)論來和大家詳細(xì)分享一下。
一、double、float類型比較
double和float都是Java中的浮點(diǎn)數(shù)類型,它們采用IEEE 754標(biāo)準(zhǔn)來表示小數(shù)。
這種表示方法類似于科學(xué)計(jì)數(shù)法,但存在一個(gè)致命問題:不是所有小數(shù)都能精確表示。
舉一個(gè)簡(jiǎn)單的例子:
double a = 0.1; double b = 0.2; double result = a + b; System.out.println(result); // 輸出:0.30000000000000004
什么?簡(jiǎn)單的0.1 + 0.2居然不等于0.3?
是的,這是因?yàn)樵诙M(jìn)制的計(jì)算中,有些十進(jìn)制小數(shù)沒有辦法精確表示,就像1除以3一樣,在十進(jìn)制中也不能精確表示(0.33333...)。
二、BigDecimal 解決方案
BigDecimal的正確用法
BigDecimal可以解決精度問題,但必須正確使用。
錯(cuò)誤的構(gòu)造方式
BigDecimal wrong1 = new BigDecimal(0.1); BigDecimal wrong2 = new BigDecimal(0.2);
這種傳入浮點(diǎn)型的構(gòu)造方式還是會(huì)有精度問題。
正確的構(gòu)造方式
BigDecimal correct1 = new BigDecimal("0.1");
BigDecimal correct2 = new BigDecimal("0.2");
System.out.println("正確方式: " + correct1.add(correct2)); // 0.3
通過傳入字符串進(jìn)行構(gòu)造。
或者用valueOf(內(nèi)部也是轉(zhuǎn)字符串)
BigDecimal safe1 = BigDecimal.valueOf(0.1);
BigDecimal safe2 = BigDecimal.valueOf(0.2);
System.out.println("安全方式: " + safe1.add(safe2)); // 0.3
BigDecimal的運(yùn)算規(guī)則
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("小計(jì): " + subtotal); // 59.97
System.out.println("稅金: " + tax); // 7.80
System.out.println("總計(jì): " + total); // 67.77
}
}
BigDecimal的比較操作
public class BigDecimalComparison {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("10.00");
BigDecimal num2 = new BigDecimal("10.000");
// 錯(cuò)誤:比較引用
System.out.println("== 比較: " + (num1 == num2)); // false
// 錯(cuò)誤:equals會(huì)比較精度
System.out.println("equals比較: " + num1.equals(num2)); // false
// 正確:compareTo只比較數(shù)值
System.out.println("compareTo比較: " + (num1.compareTo(num2) == 0)); // true
}
}
BigDecimal的缺點(diǎn)
性能開銷:對(duì)象創(chuàng)建和運(yùn)算成本高
內(nèi)存占用:每個(gè)對(duì)象需要20-30字節(jié)
使用復(fù)雜:容易用錯(cuò)構(gòu)造方法和運(yùn)算規(guī)則
代碼冗長(zhǎng):簡(jiǎn)單的運(yùn)算也需要多行代碼
三、long方案
核心思想:以分為單位存儲(chǔ)
使用long表示金額的核心思路是:不以元為單位,而以最小貨幣單位(分)存儲(chǔ)。
public class LongAmountDemo {
// 工具方法:元轉(zhuǎn)分
public static long yuanToFen(double yuan) {
return Math.round(yuan * 100);
}
// 工具方法:分轉(zhuǎn)元(用于顯示)
public static String fenToYuanDisplay(long fen) {
return String.format("¥%.2f", fen / 100.0);
}
// 工具方法:分轉(zhuǎn)元(用于計(jì)算)
public static double fenToYuan(long fen) {
return fen / 100.0;
}
public static void main(String[] args) {
// 以分為單位存儲(chǔ)所有金額
long price = yuanToFen(19.99); // 1999分 = 19.99元
long quantity = 3;
long taxRate = 13; // 13% = 0.13,這里用整數(shù)表示百分比
// 計(jì)算過程全部使用整數(shù)運(yùn)算
long subtotal = price * quantity; // 5997分
long tax = (subtotal * taxRate) / 100; // 780分
long total = subtotal + tax; // 6777分
System.out.println("小計(jì): " + fenToYuanDisplay(subtotal)); // ¥59.97
System.out.println("稅金: " + fenToYuanDisplay(tax)); // ¥7.80
System.out.println("總計(jì): " + fenToYuanDisplay(total)); // ¥67.77
}
}
long方案的優(yōu)勢(shì)
1.絕對(duì)精確 整數(shù)運(yùn)算在計(jì)算機(jī)中是精確的,不會(huì)出現(xiàn)浮點(diǎn)數(shù)的精度問題。
2.性能卓越
// long運(yùn)算 - 機(jī)器指令級(jí)別,極快
long a = 1000L, b = 2000L;
long result = a + b;
// BigDecimal運(yùn)算 - 方法調(diào)用,對(duì)象創(chuàng)建,較慢
BigDecimal c = new BigDecimal("10.00");
BigDecimal d = new BigDecimal("20.00");
BigDecimal result2 = c.add(d);
3.存儲(chǔ)高效
long:固定8字節(jié)BigDecimal:對(duì)象頭+數(shù)值,通常20-30字節(jié)
4.序列化簡(jiǎn)單 在數(shù)據(jù)庫(kù)、JSON、網(wǎng)絡(luò)傳輸中處理更簡(jiǎn)單。
實(shí)際業(yè)務(wù)應(yīng)用
public class OrderService {
// 訂單金額(分)
private long orderAmount;
// 優(yōu)惠金額(分)
private long discountAmount;
// 實(shí)付金額(分)
private long actualAmount;
public void calculateOrder(long unitPrice, int quantity, long discountRate) {
// 計(jì)算訂單金額
orderAmount = unitPrice * quantity;
// 計(jì)算優(yōu)惠金額
discountAmount = (orderAmount * discountRate) / 100;
// 計(jì)算實(shí)付金額
actualAmount = orderAmount - discountAmount;
}
// 顯示方法
public String getDisplayAmount() {
return String.format("訂單金額: %s, 優(yōu)惠: %s, 實(shí)付: %s",
formatFen(orderAmount),
formatFen(discountAmount),
formatFen(actualAmount));
}
private String formatFen(long fen) {
return String.format("¥%.2f", fen / 100.0);
}
}
四、各種方案的適用場(chǎng)景
double/float:絕對(duì)不要用于金額
- 科學(xué)計(jì)算、圖形處理
- 統(tǒng)計(jì)分析(允許誤差)
- 物理模擬
- 不適用于任何金額計(jì)算
BigDecimal:復(fù)雜金融計(jì)算
- 高精度利率、匯率計(jì)算
- 稅 務(wù)計(jì)算(需要復(fù)雜小數(shù)運(yùn)算)
- 銀行核心系統(tǒng)
- 需要任意精度的場(chǎng)景
long:大多數(shù)業(yè)務(wù)系統(tǒng)
- 電商訂單系統(tǒng)
- 支付系統(tǒng)
- 賬戶余額管理
- 積分、優(yōu)惠券系統(tǒng)
五、BigDecimal的最佳實(shí)踐
如果確實(shí)需要使用BigDecimal,請(qǐng)遵循以下規(guī)則:
1. 構(gòu)造方法
// 推薦
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.1); // 內(nèi)部轉(zhuǎn)字符串
// 避免
BigDecimal c = new BigDecimal(0.1); // 精度問題
2. 運(yùn)算控制
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. 數(shù)值比較
BigDecimal amount1 = new BigDecimal("100.00");
BigDecimal amount2 = new BigDecimal("100.000");
// 正確
if (amount1.compareTo(amount2) == 0) {
// 數(shù)值相等
}
// 錯(cuò)誤
if (amount1.equals(amount2)) {
// 不會(huì)執(zhí)行,因?yàn)榫炔煌?
}
六、long方案的實(shí)施建議
1. 建立工具類
public class MoneyUtils {
private MoneyUtils() {} // 工具類,防止實(shí)例化
// 元轉(zhuǎn)分
public static long yuanToFen(double yuan) {
return Math.round(yuan * 100);
}
// 分轉(zhuǎn)元(顯示用)
public static String fenToDisplayYuan(long fen) {
return String.format("¥%.2f", fen / 100.0);
}
// 分轉(zhuǎn)元(計(jì)算用)
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. 數(shù)據(jù)庫(kù)設(shè)計(jì)
-- 使用bigint存儲(chǔ)金額(分)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_amount BIGINT COMMENT '訂單金額(分)',
discount_amount BIGINT COMMENT '優(yōu)惠金額(分)',
actual_amount BIGINT COMMENT '實(shí)付金額(分)'
);
3. API設(shè)計(jì)
// 請(qǐng)求和響應(yīng)中使用Long類型表示金額(分)
public class OrderRequest {
private Long productId;
private Integer quantity;
private Long unitPrice; // 單價(jià)(分)
}
public class OrderResponse {
private String orderNo;
private Long totalAmount; // 總金額(分)
private String displayAmount; // 顯示金額 "¥99.99"
}
總結(jié)與選擇
為什么那個(gè)項(xiàng)目選擇long?
現(xiàn)在我可以理解那個(gè)項(xiàng)目的設(shè)計(jì)思路了:
- 業(yè)務(wù)特性:主要是簡(jiǎn)單的加減乘運(yùn)算,沒有復(fù)雜的小數(shù)除法
- 性能要求:高并發(fā)場(chǎng)景,需要最優(yōu)性能
- 團(tuán)隊(duì)協(xié)作:統(tǒng)一規(guī)范,避免BigDecimal的誤用
- 系統(tǒng)復(fù)雜度:降低系統(tǒng)復(fù)雜度和維護(hù)成本
如何選擇
| 場(chǎng)景 | 推薦方案 | 理由 |
|---|---|---|
| 簡(jiǎn)單業(yè)務(wù)系統(tǒng) | long | 性能好,簡(jiǎn)單可靠 |
| 銀行金融系統(tǒng) | BigDecimal | 精度要求極高 |
| 高并發(fā)交易 | long | 性能至關(guān)重要 |
| 復(fù)雜稅 務(wù)計(jì)算 | BigDecimal | 需要復(fù)雜小數(shù)運(yùn)算 |
| 新項(xiàng)目啟動(dòng) | long | 技術(shù)債務(wù)少 |
技術(shù)選型從來都不是單一的,理解業(yè)務(wù)的需求,能選擇出最適合的技術(shù)方案就是最好的方案。
以上就是一文淺析Java金額計(jì)算用long還是BigDecimal的詳細(xì)內(nèi)容,更多關(guān)于Java計(jì)算金額的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot打印Banner的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot啟動(dòng)Banner的實(shí)現(xiàn)原理和打印流程,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01
Java中最基本的存儲(chǔ)單元之變量基礎(chǔ)教程
變量相當(dāng)于內(nèi)存中一個(gè)數(shù)據(jù)存儲(chǔ)空間的表示,類似與門牌號(hào)與房間的關(guān)系—變量看作門牌號(hào),通過門牌號(hào)可以找到房間,而通過變量名可以訪問到變量,這篇文章主要介紹了Java中最基本的存儲(chǔ)單元之變量基礎(chǔ)教程的相關(guān)資料,需要的朋友可以參考下2025-12-12
java常用的定時(shí)任務(wù)框架及相關(guān)代碼應(yīng)用示例
在Java開發(fā)中,定時(shí)任務(wù)是一個(gè)常見的需求,比如定時(shí)備份數(shù)據(jù)、定時(shí)清理日志、定時(shí)發(fā)送郵件等,這篇文章主要介紹了java常用的定時(shí)任務(wù)框架及相關(guān)代碼應(yīng)用的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-11-11
Java圖片與Base64互轉(zhuǎn)工具類實(shí)現(xiàn)過程
本文介紹了Java中實(shí)現(xiàn)圖片與Base64編碼轉(zhuǎn)換的工具類,包括本地圖片轉(zhuǎn)Base64編碼、網(wǎng)絡(luò)圖片轉(zhuǎn)Base64編碼、Base64編碼轉(zhuǎn)圖片文件等功能,工具類使用了Java標(biāo)準(zhǔn)庫(kù)和Apache Commons庫(kù),通過Java NIO實(shí)現(xiàn)高效的文件操作,感興趣的朋友跟隨小編一起看看吧2026-01-01
Java實(shí)現(xiàn)Markdown圖片批量本地化處理工具
在日常使用Markdown時(shí),我們常通過遠(yuǎn)程URL引用圖片,所以本文小編就為大家詳細(xì)介紹一下如何使用Java實(shí)現(xiàn)Markdown圖片批量本地化處理吧2025-07-07
java面向?qū)ο蟮娜筇匦灾焕^承用法實(shí)例分析
這篇文章主要介紹了java面向?qū)ο蟮娜筇匦灾焕^承用法,結(jié)合實(shí)例形式分析了java面向?qū)ο蟪绦蛟O(shè)計(jì)中繼承的基本原理與具體使用方法,需要的朋友可以參考下2019-11-11
Java 實(shí)現(xiàn)分布式服務(wù)的調(diào)用鏈跟蹤
分布式服務(wù)中完成某一個(gè)業(yè)務(wù)動(dòng)作,需要服務(wù)之間的相互協(xié)作才能完成,在這一次動(dòng)作引起的多服務(wù)的聯(lián)動(dòng)我們需要用1個(gè)唯一標(biāo)識(shí)關(guān)聯(lián)起來,關(guān)聯(lián)起來就是調(diào)用鏈的跟蹤。本文介紹了Java 實(shí)現(xiàn)分布式服務(wù)的調(diào)用鏈跟蹤的步驟2021-06-06
MyBatis Plus插件機(jī)制與執(zhí)行流程原理分析詳解
這篇文章主要介紹了MyBatis Plus插件機(jī)制與執(zhí)行流程原理分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09

