前言
過年了,年終獎也領完了,這不打算出去面試一波,看看自己在市場中的價值,於是我簡單地做了波簡歷,然後去面試一波,結果誰知,第一個面試就差點碰壁了,面試官竟然問我BigDecimal這個類,可是我不慌,心中有料,內心不慌,於是輕鬆拿下了一波高薪offer
BigDecimal,這個類其實對於經常接觸金融、電商、支付的猿猿來說不算陌生,我也還算是熟悉,我也經常用,但是很多時候我們只知道他的用法,並不知道他還有隱藏的細節
首先,這是java.math包中提供的一種可以用來進行更高精度運算的型別,相較於double、float這些型別來說,BigDecimal在和金額計算打交道應該說有著天然的優勢,這個大家也很熟悉了,接下來我們一起來分析下BigDecimal中的哪些注意事項
1、BigDecimal不能使用equals方法做等值比較
2、BigDecimal使用double初始化時存在精度風險
問題一
這個問題其實真的是很細節了,不知道大家有沒有注意到,在《阿里巴巴Java開發手冊》中其實也有註明
不知道你們在比較BigDecimal的時候都是怎麼使用的,但是千萬不要用==這種方式來使用哦,這個應該不用多說吧,BigDecimal屬於物件,不是基本型別,不能用==來比較
一般說到這裡,大家就知道了,物件的話肯定使用equals來進行比較咯,這樣就沒問題了,告訴你,用equals比較也有問題
你個渣,我懷疑你在騙我,那你告訴我為何,還有怎麼解決?
那我該如何比較呢,自定義個類,繼承BigDecimal,重寫equals,當然可以。但是其實有更好的辦法,在BigDecimal內部提供了compareTo方法買這個方法可以直接判斷兩個數字的值,相等則返回0
知其然,也要知其所以然,我肯定會解釋清楚的嘞
我們來看個例子:
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));
以上程式碼輸出結果:true true false
有的時候結果是true,有的時候結果卻是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());
}
裡面有一個scale標度的比較,大概這就是為什麼bigDecimal5和bigDeclmal6的比較結果是false的原因了。equals不僅會比較數值,還會比較這個標度是否一樣
標度問題
使用equals進行比較的時候會比較數值大小和scale標度問題,那為什麼上面的bigDecimal1和2、bigDecimal3和4卻是相同的呢,難道是因為他們的型別是int、long,而bigDecimal5和6的型別是string,導致出現精度問題?
BigDecimal有四種定義的型別,包括int、long、double、String四種,首先int和long型別都是整數,標度都是0。當型別是double的時候,new Bigdecimal(double) => new BigDecimale(0.1),實際傳入的是0.1000000000000000055511151231527827021181583404541015625,這個時候的標度就是55,也就是小數點的個數。
而對於 new bigDecimal(1.0)來說,實際上就是整數,也就是不存在後綴,所以和整數的標度大小是一樣的
對於BigDecimal(String)來說,當我們傳入一個字串的時候,new BigDecimal("0.1")建立一個BigDecimal的時候,其實創建出來的值正好就是等於0.1的,那麼他的標題也就是1。如果使用的是new BigDecimal("0.10000"),此時標度就是5,所以這也就是解釋了為什麼最後的bigDecimal5和6的結果不一樣咯
那如何解決呢?其實BigDecimal不僅提供了equals方法,還提供了一個compareTo()方法,這個方法其實就是隻比較兩個數值的大小,感興趣的可以去研究研究
問題二
BigDecimal使用double初始化時存在精度風險,那這是怎麼一回事呢?其實在阿里開發手冊中也有這麼一條建議,或者說是要求吧
禁止使用構造方法BigDecimal(double)的方式把double值轉化成BigDecimal物件
我們知道,計算機是隻認識二進位制的,只認識0和1,也就是說任何資料都會轉化成0和1儲存在計算機中,整數簡單,除二取餘,逆序排列即可。而小數則不一定全部能轉化成二進位制,比如0.1,在轉換的過程中會出現迴圈的情況,所以這種是無法正確的儲存完整的資料的,計算機是無法精確的儲存這種資料的,所以計算機採用的是一定的精度來解決這個問題的,這就是IEEE 754(IEEE二進位制浮點數算術標準)規範的主要思想。
IEEE 754規定了多種表示浮點數值的方式,其中最常用的就是32位單精度浮點數和64位雙精度浮點數。
在Java中,使用float和double分別用來表示單精度浮點數和雙精度浮點數。
所謂精度不同,可以簡單地理解為保留有效位數不同。採用保留有效位數的方式近似的表示小數。
BigDecimal如何精確計數?
如果大家看過BigDecimal的原始碼,其實可以發現,實際上一個BigDecimal是透過一個"無標度值"和一個"標度"來表示一個數的。
在BigDecimal中,標度是透過scale欄位來表示的。
而無標度值的表示比較複雜。當unscaled value超過閾值(預設為Long.MAX_VALUE)時採用intVal欄位儲存unscaled value,intCompact欄位儲存Long.MIN_VALUE,否則對unscaled value進行壓縮儲存到long型的intCompact欄位用於後續計算,intVal為空。
涉及到的欄位就是這幾個:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
private final transient long intCompact;
}
大家只需要知道BigDecimal主要是透過一個無標度值和標度來表示的就行了。
那麼標度到底是什麼呢?除了scale這個欄位,在BigDecimal中還提供了scale()方法,用來返回這個BigDecimal的標度。那麼,scale到底表示的是什麼,其實上面的註釋已經說得很清楚了:
如果scale為零或正值,則該值表示這個數字小數點右側的位數。如果scale為負數,則該數字的真實值需要乘以10的該負數的絕對值得冪。例如,scale為-3,則這個數需要乘1000,即在末尾有3個0。
如123.123,那麼如果使用BigDecimal表示,那麼他的無標度值為123123,他的標度為3。
而二進位制無法表示的0.1,使用BigDecimal就可以表示了,及透過無標度值1和標度1來表示。
我們都知道,想要建立一個物件,需要使用該類的構造方法,在BigDecimal中一共有以下4個構造方法:
其中 BigDecimal(int)和BigDecimal(long) 比較簡單,因為都是整數,所以他們的標度都是0。而BigDecimal(double) 和BigDecimal(String)的標度就有很多學問了。
BigDecimal(double)有什麼問題
BigDecimal中雖然提供了一個透過double建立BigDecimal的方法,但是這其中也挖下了一個坑
我們知道,double表示的小數是不精確的,比如0.1這個數值,double只能表示他的近似值,所以當我們使用new BigDecimal(0.1)的時候,實際上創建出來的數值並不是正好等於0.1的,而是一個近似值
所以,如果我們在程式碼中,使用BigDecimal(double) 來建立一個BigDecimal的話,那麼是損失了精度的,這是極其嚴重的。
那麼,該如何建立一個精確的BigDecimal來表示小數呢,答案是使用String建立。
而對於BigDecimal(String) ,當我們使用new BigDecimal("0.1")建立一個BigDecimal 的時候,其實創建出來的值正好就是等於0.1的。
那麼他的標度也就是1。
但是需要注意的是,new BigDecimal("0.10000")和new BigDecimal("0.1")這兩個數的標度分別是5和1,如果使用BigDecimal的equals方法比較,得到的結果是false,可以使用compareTo方法進行比較
那麼,想要建立一個能精確的表示0.1的BigDecimal,請使用以下兩種方式:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
求贊
好了,以上就是全部內容了,我是小仙人,你們的學習成長小夥伴
我希望有一天能夠靠寫字養活自己,現在還在磨練,這個時間可能會有很多年,感謝你們做我最初的讀者和傳播者。請大家相信,只要給我一份愛,我終究會還你們一頁情的。
再次感謝大家能夠讀到這裡,我後面會持續的更新技術文章以及一些記錄生活的靈魂文章,如果覺得不錯的話,覺得【小仙】有點東西的話,求點贊、關注、分享三連
哦,對了!後續的更新文章我都會及時放到這裡,歡迎大家點選觀看,都是乾貨文章啊,建議收藏,以後隨時翻閱檢視
https://github.com/DayuMM2021/Java