BigDecimal用錯(cuò)了,哭暈在廁所......
前言
在日常開發(fā)中,很多小伙伴喜歡用 BigDecimal 來處理精確計(jì)算,比如錢、分?jǐn)?shù)、比例啥的。
理論上,它比 double 或 float 更精確,但如果你用得不對(duì),精度丟失的問題會(huì)讓你哭暈在廁所。
今天我們就來聊聊 ,錯(cuò)誤使用BigDecimal的6種場(chǎng)景,為什么會(huì)發(fā)生問題,以及怎么避免問題,希望對(duì)你會(huì)有所幫助。
1.直接用浮點(diǎn)數(shù)初始化
不少小伙伴習(xí)慣這樣寫:
BigDecimal num = new BigDecimal(0.1);
System.out.println(num);
打印結(jié)果:0.1000000000000000055511151231257827021181583404541015625
并非打印的:0.1
問題出在哪?
這不是 BigDecimal 的問題,而是浮點(diǎn)數(shù)本身的“鍋”。
在Java中,double的精度有限的,0.1 轉(zhuǎn)換成二進(jìn)制是個(gè)無限循環(huán)小數(shù),直接傳進(jìn)去會(huì)帶上誤差。
正確姿勢(shì)是傳字符串:
BigDecimal num = new BigDecimal("0.1");
System.out.println(num);
打印結(jié)果:0.1,是正確的。
注意:永遠(yuǎn)不要用 BigDecimal(double) 構(gòu)造函數(shù),用字符串或整數(shù)更靠譜。也可以使用BigDecimal.valueOf()函數(shù)。
2.加減乘除時(shí)不設(shè)精度
有些小伙伴做加減乘除的時(shí)候,直接寫:
BigDecimal a = new BigDecimal("1.03");
BigDecimal b = new BigDecimal("0.42");
//減法
BigDecimal result = a.subtract(b);
System.out.println(result);
打印結(jié)果::0.61,沒問題。
但問題在 除法 時(shí):
BigDecimal c = new BigDecimal("10");
BigDecimal d = new BigDecimal("3");
BigDecimal result = c.divide(d);
運(yùn)行直接炸了:java.lang.ArithmeticException: Non-terminating decimal expansion
報(bào)錯(cuò)的根本原因:10/3 是無限小數(shù),BigDecimal 默認(rèn)不保留小數(shù)點(diǎn)后面,精度溢出。
那么,我們要如何優(yōu)化呢?
答:加一個(gè) MathContext 或指定精度。
例如:
BigDecimal result = c.divide(d, 2, RoundingMode.HALF_UP);
System.out.println(result);
打印結(jié)果:3.33,可以正常運(yùn)行。
因此,我們需要注意,在BigDecimal 做除法時(shí) ,必須指定精度。
3.用 equals 判斷相等
BigDecimal 的 equals 會(huì)比較 值和精度,這坑了不少人:
BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y));
打印結(jié)果:false。
盡管 1.0 和 1.00 的數(shù)值相等,但精度不一樣,equals 判定為不同。
優(yōu)化方法,用 compareTo 比較數(shù)值:
例如:
System.out.println(x.compareTo(y) == 0);
打印結(jié)果:true
需要特別注意的地方是:我們?cè)谂袛鄡蓚€(gè)BigDecimal對(duì)象是否相等時(shí),應(yīng)該用 compareTo方法,別用 equals方法。
4.使用 scale 時(shí)忽視實(shí)際含義
有些小伙伴搞不清 scale(小數(shù)位數(shù))和 precision(總位數(shù))的區(qū)別,直接寫:
BigDecimal num = new BigDecimal("123.4500");
System.out.println(num.scale());
打印結(jié)果:4
但如果你寫成下面這樣的:
BigDecimal stripped = num.stripTrailingZeros();
System.out.println(stripped.scale());
打印結(jié)果卻是:1
scale 會(huì)發(fā)生變化,搞不好會(huì)影響后續(xù)計(jì)算。
那么,我們要如何優(yōu)化方法呢?
答:明確 scale 的含義。
如果要固定小數(shù)位,使用 setScale:
BigDecimal fixed = num.setScale(2, RoundingMode.HALF_UP);
System.out.println(fixed);
打印結(jié)果:123.45。
我們不要混淆 scale 和 precision,必要時(shí)顯式設(shè)置小數(shù)位數(shù)。
5.忽略不可變性
BigDecimal 是不可變的,但有些小伙伴會(huì)這樣寫:
BigDecimal sum = new BigDecimal("0");
for (int i = 0; i < 5; i++) {
sum.add(new BigDecimal("1"));
}
打印結(jié)果:0
問題原因是 add 方法不會(huì)改變?cè)瓕?duì)象,而是返回一個(gè)新的 BigDecimal 實(shí)例。
那么,我們要如何優(yōu)化呢?
答:用變量接住返回值。
BigDecimal sum = new BigDecimal("0");
for (int i = 0; i < 5; i++) {
sum = sum.add(new BigDecimal("1"));
}
System.out.println(sum);
打印結(jié)果是:5
BigDecimal 操作后需要接住新實(shí)例。
6.忽視性能問題
BigDecimal 是很精確,但也很慢。
如果大量計(jì)算時(shí)用 BigDecimal,會(huì)拖累性能,比如計(jì)算利息:
BigDecimal principal = new BigDecimal("10000");
BigDecimal rate = new BigDecimal("0.05");
BigDecimal interest = principal.multiply(rate);
一個(gè)循環(huán)里搞上百萬次,性能直接拉垮。
那么,這種情況我們又該如何優(yōu)化呢?
答:能用整數(shù)就用整數(shù)(比如分代替元)。
批量計(jì)算時(shí),用 double 計(jì)算,結(jié)果最后轉(zhuǎn)換成 BigDecimal。
double principal = 10000;
double rate = 0.05;
BigDecimal interest = BigDecimal.valueOf(principal * rate);
System.out.println(interest);
打印結(jié)果:500.00
參與大批量計(jì)算時(shí),兩個(gè)BigDecimal對(duì)象直接計(jì)算會(huì)比較慢,盡量少用,能優(yōu)化的地方別放過。
寫在最后
BigDecimal 是個(gè)非常強(qiáng)大的數(shù)字類工具,但也是個(gè)“細(xì)節(jié)狂魔”。
只有用對(duì)了,你才能真正享受它帶來的好處,否則就是自找麻煩。
希望這篇文章能幫到你,不要再踩坑。