Java使用BigDecimal解決精度丟失問(wèn)題
一、先搞懂:為什么浮點(diǎn)型會(huì)丟精度?
我們常用的float和double,屬于二進(jìn)制浮點(diǎn)類型,但現(xiàn)實(shí)中我們計(jì)算的“金額(如1.2元)”“重量(如3.5kg)”是十進(jìn)制數(shù)據(jù)——這兩種進(jìn)制的“不兼容”,就是精度丟失的根源。
舉個(gè)最直觀的例子,運(yùn)行這段代碼:
public class FloatTest {
public static void main(String[] args) {
System.out.println(0.1 + 0.2); // 輸出結(jié)果不是0.3,而是0.30000000000000004
System.out.println(1.0 - 0.9); // 輸出0.09999999999999998
System.out.println(2.01 * 100); // 輸出200.99999999999997
}
}
為什么會(huì)這樣?
因?yàn)?strong>0.1在二進(jìn)制中是“無(wú)限循環(huán)小數(shù)” (類似十進(jìn)制的1/3=0.333...),而float和double的存儲(chǔ)位數(shù)有限(float占4字節(jié),double占8字節(jié)),只能“四舍五入”保留部分二進(jìn)制位——這種“截?cái)?rdquo;就導(dǎo)致了精度丟失,后續(xù)計(jì)算會(huì)把誤差放大,最終出現(xiàn)“0.1+0.2≠0.3”的詭異結(jié)果。
重點(diǎn):只要涉及“需要精確計(jì)算”的場(chǎng)景(金額、財(cái)務(wù)、計(jì)量),絕對(duì)不能用float/double! 這不是“代碼寫(xiě)錯(cuò)了”,而是數(shù)據(jù)類型的底層特性決定的。
二、救星BigDecimal:但90%的人用錯(cuò)了
很多人知道“用BigDecimal代替浮點(diǎn)型”,但我見(jiàn)過(guò)太多“用了BigDecimal還丟精度”的情況——問(wèn)題出在構(gòu)造方法和計(jì)算方式上,這兩個(gè)坑一定要避開(kāi)。
坑1:用double構(gòu)造BigDecimal(最常見(jiàn)錯(cuò)誤)
直接用new BigDecimal(double)構(gòu)造,會(huì)把double的精度誤差“帶進(jìn)去”,比如:
// 錯(cuò)誤用法:用double構(gòu)造,精度誤差被保留
BigDecimal wrong1 = new BigDecimal(0.1);
System.out.println(wrong1); // 輸出0.1000000000000000055511151231257827021181583404541015625
// 正確用法:用String構(gòu)造,完全保留十進(jìn)制精度
BigDecimal right1 = new BigDecimal("0.1");
System.out.println(right1); // 輸出0.1
// 也可以用BigDecimal.valueOf(double)(底層會(huì)轉(zhuǎn)成String,推薦)
BigDecimal right2 = BigDecimal.valueOf(0.1);
System.out.println(right2); // 輸出0.1結(jié)論:構(gòu)造BigDecimal時(shí),優(yōu)先用new BigDecimal(String)或BigDecimal.valueOf(double),絕對(duì)別用new BigDecimal(double)!
坑2:用“+、-、*、/”直接計(jì)算(編譯都不通過(guò))
BigDecimal是“對(duì)象”,不能像基本類型那樣用算術(shù)運(yùn)算符計(jì)算,必須用它的成員方法(add/subtract/multiply/divide),且計(jì)算時(shí)要指定“舍入模式”(避免除不盡時(shí)拋異常)。
正確計(jì)算示例(以金額計(jì)算為例):
public class BigDecimalCalc {
public static void main(String[] args) {
// 1. 初始化金額(單位:元,用String構(gòu)造)
BigDecimal price = new BigDecimal("99.9"); // 商品單價(jià)
BigDecimal quantity = new BigDecimal("3"); // 購(gòu)買數(shù)量
BigDecimal discount = new BigDecimal("0.9"); // 9折優(yōu)惠
// 2. 計(jì)算:總價(jià) = 單價(jià) * 數(shù)量 * 折扣(用成員方法)
BigDecimal total = price.multiply(quantity).multiply(discount);
System.out.println("折后總價(jià):" + total); // 輸出269.73
// 3. 除法示例(如拆分金額,必須指定舍入模式)
BigDecimal split = total.divide(new BigDecimal("2"), 2, BigDecimal.ROUND_HALF_UP);
System.out.println("每人分?jǐn)偅? + split); // 輸出134.87(四舍五入保留2位小數(shù))
}
}
關(guān)鍵:舍入模式怎么選?(金額計(jì)算常用3種)
| 舍入模式常量 | 含義(以保留2位小數(shù)為例) | 適用場(chǎng)景 |
|---|---|---|
ROUND_HALF_UP | 四舍五入(1.234→1.23,1.235→1.24) | 金額計(jì)算、日常統(tǒng)計(jì) |
ROUND_DOWN | 直接截?cái)啵?.239→1.23) | 計(jì)算最小支付金額(不進(jìn)位) |
ROUND_UP | 直接進(jìn)位(1.231→1.24) | 計(jì)算稅費(fèi)(不遺漏分厘) |
注意:divide方法必須指定“舍入模式”和“保留小數(shù)位數(shù)”,否則當(dāng)除法結(jié)果是無(wú)限小數(shù)時(shí)(如1÷3),會(huì)拋出ArithmeticException!
三、實(shí)戰(zhàn)避坑:金額計(jì)算的3個(gè)“固定套路”
結(jié)合8年項(xiàng)目經(jīng)驗(yàn),總結(jié)出“金額計(jì)算(元為單位)”的標(biāo)準(zhǔn)化寫(xiě)法,直接套用能避免99%的問(wèn)題:
套路1:定義“保留小數(shù)位數(shù)”和“舍入模式”常量
避免硬編碼,后續(xù)修改更方便:
// 金額計(jì)算:固定保留2位小數(shù),四舍五入 private static final int SCALE = 2; private static final int ROUND_MODE = BigDecimal.ROUND_HALF_UP;
套路2:封裝“加減乘除”工具方法
重復(fù)代碼抽成工具類,減少重復(fù)錯(cuò)誤:
public class MoneyUtil {
private static final int SCALE = 2;
private static final int ROUND_MODE = BigDecimal.ROUND_HALF_UP;
// 加法
public static BigDecimal add(BigDecimal a, BigDecimal b) {
return a.add(b).setScale(SCALE, ROUND_MODE);
}
// 減法
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
return a.subtract(b).setScale(SCALE, ROUND_MODE);
}
// 乘法
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
return a.multiply(b).setScale(SCALE, ROUND_MODE);
}
// 除法
public static BigDecimal divide(BigDecimal a, BigDecimal b) {
if (BigDecimal.ZERO.compareTo(b) == 0) {
throw new IllegalArgumentException("除數(shù)不能為0");
}
return a.divide(b, SCALE, ROUND_MODE);
}
}
套路3:和數(shù)據(jù)庫(kù)交互時(shí)的“類型對(duì)應(yīng)”
如果數(shù)據(jù)庫(kù)存儲(chǔ)金額用DECIMAL類型(推薦),Java中用BigDecimal接收,避免類型轉(zhuǎn)換丟失精度:
- 數(shù)據(jù)庫(kù)字段定義:
amount DECIMAL(10,2)(10位整數(shù)+2位小數(shù),足夠存儲(chǔ)千萬(wàn)級(jí)金額) - MyBatis映射:直接用
java.math.BigDecimal對(duì)應(yīng),不要用double接收
四、知識(shí)擴(kuò)展
在 Java 中,float 和 double 用于科學(xué)計(jì)算和工程計(jì)算,但由于它們采用二進(jìn)制浮點(diǎn)數(shù)表示,無(wú)法精確表示某些十進(jìn)制小數(shù)(如 0.1),從而導(dǎo)致精度丟失。
示例:
double a = 0.1; double b = 0.2; System.out.println(a + b); // 0.30000000000000004
為了進(jìn)行精確的十進(jìn)制運(yùn)算(如金額計(jì)算),Java 提供了 BigDecimal 類。它通過(guò)用整數(shù)(BigInteger)和標(biāo)度(小數(shù)點(diǎn)后位數(shù))的組合來(lái)精確表示任意精度的十進(jìn)制數(shù)。
正確創(chuàng)建 BigDecimal 對(duì)象
錯(cuò)誤方式:使用 double 構(gòu)造方法
BigDecimal bd = new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181583404541015625
因?yàn)?nbsp;0.1 本身已經(jīng)是近似值,所以傳入 double 就失去了精度。
正確方式:使用字符串構(gòu)造器
BigDecimal bd = new BigDecimal("0.1");推薦方式:使用 BigDecimal.valueOf(double)如果參數(shù)是 double 字面量或已知精確值,可使用靜態(tài)方法:
BigDecimal bd = BigDecimal.valueOf(0.1);
該方法內(nèi)部先將 double 轉(zhuǎn)換為字符串再構(gòu)造,能避免部分精度問(wèn)題。
基本運(yùn)算(加減乘除)
BigDecimal a = new BigDecimal("10.25");
BigDecimal b = new BigDecimal("3.05");
// 加法
BigDecimal sum = a.add(b); // 13.30
// 減法
BigDecimal diff = a.subtract(b); // 7.20
// 乘法
BigDecimal prod = a.multiply(b); // 31.2625
// 除法(必須指定舍入模式,否則可能拋出 ArithmeticException)
BigDecimal quot = a.divide(b, 2, RoundingMode.HALF_UP); // 3.36
// 參數(shù):除數(shù),保留小數(shù)位數(shù),舍入模式關(guān)鍵:除法必須指定精度和舍入模式,避免無(wú)限循環(huán)小數(shù)。
精度控制與舍入模式
| 舍入模式 | 說(shuō)明 |
|---|---|
RoundingMode.HALF_UP | 四舍五入(最常用) |
RoundingMode.HALF_DOWN | 五舍六入 |
RoundingMode.UP | 遠(yuǎn)離零方向舍入 |
RoundingMode.DOWN | 向零方向舍入 |
RoundingMode.CEILING | 向正無(wú)窮方向舍入 |
RoundingMode.FLOOR | 向負(fù)無(wú)窮方向舍入 |
RoundingMode.HALF_EVEN | 向偶數(shù)舍入(銀行家舍入法) |
BigDecimal d = new BigDecimal("2.335");
System.out.println(d.setScale(2, RoundingMode.HALF_UP)); // 2.34
System.out.println(d.setScale(2, RoundingMode.HALF_DOWN)); // 2.33比較 BigDecimal 對(duì)象
錯(cuò)誤方式:使用 equalsequals 會(huì)比較精度(scale),例如 new BigDecimal("2.0") 與 new BigDecimal("2.00") 不相等。
正確方式:使用 compareTo
BigDecimal x = new BigDecimal("2.0");
BigDecimal y = new BigDecimal("2.00");
System.out.println(x.compareTo(y) == 0); // truecompareTo 返回 -1(小于)、0(等于)、1(大于)。
五、最后總結(jié):3句話避開(kāi)浮點(diǎn)精度坑
- 場(chǎng)景判斷:只要是“需要精確到分/厘”的計(jì)算(金額、財(cái)務(wù)),絕對(duì)不用float/double;
- 構(gòu)造正確:BigDecimal用String構(gòu)造或valueOf(double),別用double構(gòu)造;
- 計(jì)算規(guī)范:用成員方法(add/subtract),指定舍入模式和保留位數(shù),優(yōu)先封裝工具類。
到此這篇關(guān)于Java使用BigDecimal解決精度丟失問(wèn)題的文章就介紹到這了,更多相關(guān)Java BigDecimal解決精度丟失內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java?BigDecimal?解決浮點(diǎn)精度問(wèn)題(用法實(shí)踐)
- Java使用BigDecimal確保數(shù)值計(jì)算精度的最佳實(shí)踐指南
- 淺析Java?BigDecimal為什么可以不丟失精度
- Java BigDecimal解決double精度丟失的問(wèn)題
- Java中的BigDecimal精度運(yùn)算詳解
- java BigDecimal精度丟失及常見(jiàn)問(wèn)分析
- Java使用BigDecimal公式精確計(jì)算及精度丟失問(wèn)題
- Java中BigDecimal除法使用不當(dāng)導(dǎo)致精度問(wèn)題
- Java BigDecimal除法精度和格式化輸出方式
相關(guān)文章
談?wù)勎覍?duì)Spring Bean 生命周期的理解
Spring Bean 的生命周期在整個(gè) Spring 中占有很重要的位置,掌握這些可以加深對(duì) Spring 的理解。這篇文章主要介紹了Spring Bean 生命周期,需要的朋友可以參考下2018-03-03
java圖的深度優(yōu)先遍歷實(shí)現(xiàn)隨機(jī)生成迷宮
這篇文章主要為大家詳細(xì)介紹了java圖的深度優(yōu)先遍歷實(shí)現(xiàn)隨機(jī)生成迷宮,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
面向?qū)ο缶幊?Java中的抽象數(shù)據(jù)類型
面向?qū)ο缶幊?Java中的抽象數(shù)據(jù)類型...2006-12-12
Java數(shù)據(jù)結(jié)構(gòu)之平衡二叉樹(shù)的實(shí)現(xiàn)詳解
平衡二叉樹(shù)又被稱為AVL樹(shù)(有別于AVL算法),且具有以下性質(zhì):它是一棵空樹(shù)或它的左右兩個(gè)子樹(shù)的高度差的絕對(duì)值不超過(guò)1,并且左右兩個(gè)子樹(shù)都是一棵平衡二叉樹(shù)。本文將詳解介紹一下平衡二叉樹(shù)的原理與實(shí)現(xiàn),需要的可以參考一下2022-03-03
Spring boot如何通過(guò)@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)及多線程配置
這篇文章主要介紹了Spring boot如何通過(guò)@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)及多線程配置,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12

