面試官問我:你確定用了BigDecimal后,計(jì)算結(jié)果一定精確?

過年了,年終也領(lǐng)完了,這不打算出去面試一波,看看自己在市場中的價(jià)值,于是我簡單的做了波簡歷,然后去面試一波,結(jié)果誰知,第一個(gè)面試就差點(diǎn)碰壁了,面試官竟然問我BigDecimal這個(gè)類,可是我不慌,心中有料,內(nèi)心不慌,于是輕松拿下了一波高薪offer
BigDecimal,這個(gè)類其實(shí)對于經(jīng)常接觸金融、電商、支付的猿猿來說不算陌生,我也還算是熟悉,我也經(jīng)常用,但是很多時(shí)候我們只知道他的用法,并不知道他還有隱藏的細(xì)節(jié)
首先,這是java.math包中提供的一種可以用來進(jìn)行更高精度運(yùn)算的類型,相較于double、float這些類型來說,BigDecimal在和金額計(jì)算打交道應(yīng)該說有著天然的優(yōu)勢,這個(gè)大家也很熟悉了,接下來我們一起來分析下BigDecimal中的哪些注意事項(xiàng)
1、BigDecimal不能使用equals方法做等值比較
2、BigDecimal使用double初始化時(shí)存在精度風(fēng)險(xiǎn)

這個(gè)問題其實(shí)真的是很細(xì)節(jié)了,不知道大家有沒有注意到,在《阿里巴巴Java開發(fā)手冊》中其實(shí)也有注明
不知道你們在比較BigDecimal的時(shí)候都是怎么使用的,但是千萬不要用==這種方式來使用哦,這個(gè)應(yīng)該不用多說吧,BigDecimal屬于對象,不是基本類型,不能用==來比較
一般說到這里,大家就知道了,對象的話肯定使用equals來進(jìn)行比較咯,這樣就沒問題了,告訴你,用equals比較也有問題
你個(gè)渣,我懷疑你在騙我,那你告訴我為何,還有怎么解決?
那我該如何比較呢,自定義個(gè)類,繼承BigDecimal,重寫equals,當(dāng)然可以。但是其實(shí)有更好的辦法,在BigDecimal內(nèi)部提供了compareTo方法買這個(gè)方法可以直接判斷兩個(gè)數(shù)字的值,相等則返回0
知其然,也要知其所以然,我肯定會解釋清楚的嘞
我們來看個(gè)例子:
BigDecimal bigDecimal1 = new BigDecimal(1);
BigDecimal bigDecimal2 = new BigDecimal(1);
System.out.println(bigDecimal1.equals(bigDecimal2));
BigDecimal bigDecimal3 = new BigDecimal(1);
BigDecimal bigDecimal4 = new BigDecimal(1.0);
System.out.println(bigDecimal3.equals(bigDecimal4));
BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");
System.out.println(bigDecimal5.equals(bigDecimal6));以上代碼輸出結(jié)果:true ?true ?false
有的時(shí)候結(jié)果是true,有的時(shí)候結(jié)果卻是false,很奇怪,為什么呢?我們來看下BigDecimal的equals的源碼:
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}里面有一個(gè)scale標(biāo)度的比較,大概這就是為什么bigDecimal5和bigDeclmal6的比較結(jié)果是false的原因了。equals不僅會比較數(shù)值,還會比較這個(gè)標(biāo)度是否一樣
使用equals進(jìn)行比較的時(shí)候會比較數(shù)值大小和scale標(biāo)度問題,那為什么上面的bigDecimal1和2、bigDecimal3和4卻是相同的呢,難道是因?yàn)樗麄兊念愋褪莍nt、long,而bigDecimal5和6的類型是string,導(dǎo)致出現(xiàn)精度問題?
BigDecimal有四種定義的類型,包括int、long、double、String四種,首先int和long類型都是整數(shù),標(biāo)度都是0。當(dāng)類型是double的時(shí)候,new Bigdecimal(double)? => new BigDecimale(0.1),實(shí)際傳入的是0.1000000000000000055511151231527827021181583404541015625,這個(gè)時(shí)候的標(biāo)度就是55,也就是小數(shù)點(diǎn)的個(gè)數(shù)。
而對于 new bigDecimal(1.0)來說,實(shí)際上就是整數(shù),也就是不存在后綴,所以和整數(shù)的標(biāo)度大小是一樣的
對于BigDecimal(String)來說,當(dāng)我們傳入一個(gè)字符串的時(shí)候,new BigDecimal("0.1")創(chuàng)建一個(gè)BigDecimal的時(shí)候,其實(shí)創(chuàng)建出來的值正好就是等于0.1的,那么他的標(biāo)題也就是1。如果使用的是new BigDecimal("0.10000"),此時(shí)標(biāo)度就是5,所以這也就是解釋了為什么最后的bigDecimal5和6的結(jié)果不一樣咯
那如何解決呢?其實(shí)BigDecimal不僅提供了equals方法,還提供了一個(gè)compareTo()方法,這個(gè)方法其實(shí)就是只比較兩個(gè)數(shù)值的大小,感興趣的可以去研究研究

BigDecimal使用double初始化時(shí)存在精度風(fēng)險(xiǎn),那這是怎么一回事呢?其實(shí)在阿里開發(fā)手冊中也有這么一條建議,或者說是要求吧

禁止使用構(gòu)造方法BigDecimal(double)的方式把double值轉(zhuǎn)化成BigDecimal對象
我們知道,計(jì)算機(jī)是只認(rèn)識二進(jìn)制的,只認(rèn)識0和1,也就是說任何數(shù)據(jù)都會轉(zhuǎn)化成0和1存儲在計(jì)算機(jī)中,整數(shù)簡單,除二取余,逆序排列即可。而小數(shù)則不一定全部能轉(zhuǎn)化成二進(jìn)制,比如0.1,在轉(zhuǎn)換的過程中會出現(xiàn)循環(huán)的情況,所以這種事無法正確的存儲完整的數(shù)據(jù)的,計(jì)算機(jī)是無法精確的存儲這種數(shù)據(jù)的,所以計(jì)算機(jī)采用的是一定的精度來解決這個(gè)問題的,這就是IEEE 754(IEEE二進(jìn)制浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn))規(guī)范的主要思想。
IEEE 754規(guī)定了多種表示浮點(diǎn)數(shù)值的方式,其中最常用的就是32位單精度浮點(diǎn)數(shù)和64位雙精度浮點(diǎn)數(shù)。
在Java中,使用float和double分別用來表示單精度浮點(diǎn)數(shù)和雙精度浮點(diǎn)數(shù)。
所謂精度不同,可以簡單的理解為保留有效位數(shù)不同。采用保留有效位數(shù)的方式近似的表示小數(shù)。
如果大家看過BigDecimal的源碼,其實(shí)可以發(fā)現(xiàn),實(shí)際上一個(gè)BigDecimal是通過一個(gè)"無標(biāo)度值"和一個(gè)"標(biāo)度"來表示一個(gè)數(shù)的。
在BigDecimal中,標(biāo)度是通過scale字段來表示的。
而無標(biāo)度值的表示比較復(fù)雜。當(dāng)unscaled value超過閾值(默認(rèn)為Long.MAX_VALUE)時(shí)采用intVal字段存儲unscaled value,intCompact字段存儲Long.MIN_VALUE,否則對unscaled value進(jìn)行壓縮存儲到long型的intCompact字段用于后續(xù)計(jì)算,intVal為空。
涉及到的字段就是這幾個(gè):
?public?class?BigDecimal?extends?Number?implements?Comparable<BigDecimal>?{
????????private?final?BigInteger?intVal;
????????private?final?int?scale;?
????????private?final?transient?long?intCompact;
????}
大家只需要知道BigDecimal主要是通過一個(gè)無標(biāo)度值和標(biāo)度來表示的就行了。
那么標(biāo)度到底是什么呢?除了scale這個(gè)字段,在BigDecimal中還提供了scale()方法,用來返回這個(gè)BigDecimal的標(biāo)度。那么,scale到底表示的是什么,其實(shí)上面的注釋已經(jīng)說的很清楚了:
如果scale為零或正值,則該值表示這個(gè)數(shù)字小數(shù)點(diǎn)右側(cè)的位數(shù)。如果scale為負(fù)數(shù),則該數(shù)字的真實(shí)值需要乘以10的該負(fù)數(shù)的絕對值的冪。例如,scale為-3,則這個(gè)數(shù)需要乘1000,即在末尾有3個(gè)0。
如123.123,那么如果使用BigDecimal表示,那么他的無標(biāo)度值為123123,他的標(biāo)度為3。
而二進(jìn)制無法表示的0.1,使用BigDecimal就可以表示了,及通過無標(biāo)度值1和標(biāo)度1來表示。
我們都知道,想要?jiǎng)?chuàng)建一個(gè)對象,需要使用該類的構(gòu)造方法,在BigDecimal中一共有以下4個(gè)構(gòu)造方法:
其中 BigDecimal(int)和BigDecimal(long) 比較簡單,因?yàn)槎际钦麛?shù),所以他們的標(biāo)度都是0。而BigDecimal(double) 和BigDecimal(String)的標(biāo)度就有很多學(xué)問了。
BigDecimal中雖然提供了一個(gè)通過double創(chuàng)建BigDecimal的方法,但是這其中也挖下了一個(gè)坑
我們知道,double表示的小數(shù)是不精確的,比如0.1這個(gè)數(shù)值,double只能表示他的近似值,所以當(dāng)我們使用new BigDecimal(0.1)的時(shí)候,實(shí)際上創(chuàng)建出來的數(shù)值并不是正好等于0.1的,而是一個(gè)近似值
所以,如果我們在代碼中,使用BigDecimal(double) 來創(chuàng)建一個(gè)BigDecimal的話,那么是損失了精度的,這是極其嚴(yán)重的。
那么,該如何創(chuàng)建一個(gè)精確的BigDecimal來表示小數(shù)呢,答案是使用String創(chuàng)建。
而對于BigDecimal(String) ,當(dāng)我們使用new BigDecimal("0.1")創(chuàng)建一個(gè)BigDecimal 的時(shí)候,其實(shí)創(chuàng)建出來的值正好就是等于0.1的。
那么他的標(biāo)度也就是1。
但是需要注意的是,new BigDecimal("0.10000")和new BigDecimal("0.1")這兩個(gè)數(shù)的標(biāo)度分別是5和1,如果使用BigDecimal的equals方法比較,得到的結(jié)果是false,可以使用compareTo方法進(jìn)行比較
那么,想要?jiǎng)?chuàng)建一個(gè)能精確的表示0.1的BigDecimal,請使用以下兩種方式:
????BigDecimal?recommend1?=?new?BigDecimal("0.1");
????BigDecimal?recommend2?=?BigDecimal.valueOf(0.1);

?

?
好了,以上就是全部內(nèi)容了,我是小仙人,你們的學(xué)習(xí)成長小伙伴
? ? ? ? ? ? ? ? ? ??
我希望有一天能夠靠寫字養(yǎng)活自己,現(xiàn)在還在磨練,這個(gè)時(shí)間可能會有很多年,感謝你們做我最初的讀者和傳播者。請大家相信,只要給我一份愛,我終究會還你們一頁情的。
再次感謝大家能夠讀到這里,我后面會持續(xù)的更新技術(shù)文章以及一些記錄生活的靈魂文章,如果覺得不錯(cuò)的,覺得【小仙】有點(diǎn)東西的話,求點(diǎn)贊、關(guān)注、分享三連
哦,對了!后續(xù)的更新文章我都會及時(shí)放到這里,歡迎大家點(diǎn)擊觀看,都是干貨文章啊,建議收藏,以后隨時(shí)翻閱查看
https://github.com/DayuMM2021/Java
?
?
