上周看到一篇因為在金額計算中沒有使用BigDecimal而導致故障的文章,但是除非在一些非常簡單的場景,結算匯金類的業務也不會直接用BigDecimal來計算金額,原因有兩點:
先看下面這段代碼
BigDecimalbd1=newBigDecimal(0.01);BigDecimalbd2=BigDecimal.valueOf(0.01);System.out.println("bd1="+bd1);System.out.println("bd2="+bd2);輸出到控制台的結果是:
bd1=0.01000000000000000020816681711721685132943093776702880859375bd2=0.01造成這種差異的原因是0.1這個數字計算機是無法精確表示的,送給BigDecimal的時候就已經丟精度了,而BigDecimal#valueOf的實現卻完全不同
publicstaticBigDecimalvalueOf(doubleval){//Reminder:azerodoublereturns'0.0',sowecannotfastpath//tousetheconstantZERO.Thismightbeimportantenoughto//justifyafactoryapproach,acache,orafewprivate//constants,later.returnnewBigDecimal(Double.toString(val));}它使用了浮點數相應的字符串來構造BigDecimal對象,因此避免了精度問題。所以大家要儘量要使用字符串而不是浮點數去構造BigDecimal對象,如果實在不行,就使用BigDecimal#valueOf()方法吧。
1.2 等值比較BigDecimalbd1=newBigDecimal("1.0");BigDecimalbd2=newBigDecimal("1.00");System.out.println(bd1.equals(bd2));System.out.println(bd1.compareTo(bd2));控制台的輸出將會是:
false0究其原因是,BigDecimal中equals方法的實現會比較兩個數字的精度,而compareTo方法則只會比較數值的大小。
1.3 BigDecimal並不代表無限精度先看這段代碼
BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("3.0");a.divide(b)//resultsinthefollowingexception.結果會拋出異常:
java.lang.ArithmeticException:Non-terminatingdecimalexpansion;noexactrepresentabledecimalresult.關於這個異常,Oracle的官方文檔有具體說明
If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.
大意是,如果除法的商的結果是一個無限小數但是我們期望返回精確的結果,那程序就會拋出異常。回到我們的這個例子,我們需要告訴JVM我們不需要返回精確的結果就好了
BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("3.0");a.divide(b,2,RoundingMode.HALF_UP)//0.331.4 BigDecimal轉回String要小心BigDecimald=BigDecimal.valueOf(12334535345456700.12345634534534578901);Stringout=d.toString();//OrperformanyformattingthatneedstobedoneSystem.out.println(out);//1.23345353454567E+16可以看到結果已經被轉換成了科學計數法,可能這個並不是預期的結果BigDecimal有三個方法可以轉為相應的字符串類型,切記不要用錯:
StringtoString();//有必要時使用科學計數法StringtoPlainString();//不使用科學計數法StringtoEngineeringString();//工程計算中經常使用的記錄數字的方法,與科學計數法類似,但要求10的冪必須是3的倍數1.5 執行順序不能調換(乘法交換律失效)乘法滿足交換律是一個常識,但是在計算機的世界裡,會出現不滿足乘法交換律的情況
BigDecimala=BigDecimal.valueOf(1.0);BigDecimalb=BigDecimal.valueOf(3.0);BigDecimalc=BigDecimal.valueOf(3.0);System.out.println(a.divide(b,2,RoundingMode.HALF_UP).multiply(c));//0.990System.out.println(a.multiply(c).divide(b,2,RoundingMode.HALF_UP));//1.00別小看這這0.01的差別,在匯金領域,會產生非常大的金額差異。
2. 最佳實踐關於金額計算,很多業務團隊會基於BigDecimal再封裝一個Money類,其實我們直接可以用一個半官方的Money類:JSR 354 ,雖然沒能在Java 9中成為Java標準,很有可能集成到後續的Java版本中成為官方庫。
2.1 maven坐標<dependency><groupId>org.javamoney</groupId><artifactId>moneta</artifactId><version>1.1</version></dependency>2.2 新建Money類CurrencyUnitcny=Monetary.getCurrency("CNY");Moneymoney=Money.of(1.0,cny);//或者Moneymoney=Money.of(1.0,"CNY");//System.out.println(money);2.3 金額運算CurrencyUnitcny=Monetary.getCurrency("CNY");MoneyoneYuan=Money.of(1.0,cny);MoneythreeYuan=oneYuan.add(Money.of(2.0,"CNY"));//CNY3MoneytenYuan=oneYuan.multiply(10);//CNY10MoneyfiveFen=oneYuan.divide(2);//CNY0.52.4 比較相等MoneyfiveFen=Money.of(0.5,"CNY");//CNY0.5MoneyanotherFiveFen=Money.of(0.50,"CNY");//CNY0.50System.out.println(fiveFen.equals(anotherFiveFen));//true可以看到,這個類對金額做了顯性的抽象,增加了金額的單位,也避免了直接使用BigDecimal的一些坑。
推薦
Java面試題寶典
技術內卷群,一起來學習!!
PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!