金額計算用 BigDecimal 就萬無一失了?看看這五個坑吧~~
Hollis的新書限時折扣中,一本深入講解Java基礎的干貨筆記!
看到一篇因為在金額計算中沒有使用BigDecimal而導致故障的文章,但是除非在一些非常簡單的場景,結算匯金類的業(yè)務也不會直接用BigDecimal來計算金額,原因有兩點:
BigDecimal里面還是有很多隱蔽的坑的BigDecimal沒有提供金額的單位
1. BigDecimal中的五個容易踩的坑
1.1 new BigDecimal()還是BigDecimal#valueOf()?
先看下面這段代碼
BigDecimal bd1 = new BigDecimal(0.01);
BigDecimal bd2 = BigDecimal.valueOf(0.01);
System.out.println("bd1 = " + bd1);
System.out.println("bd2 = " + bd2);
輸出到控制臺的結果是:
bd1 = 0.01000000000000000020816681711721685132943093776702880859375
bd2 = 0.01
造成這種差異的原因是0.1這個數(shù)字計算機是無法精確表示的,送給BigDecimal的時候就已經(jīng)丟精度了,而BigDecimal#valueOf的實現(xiàn)卻完全不同
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
它使用了浮點數(shù)相應的字符串來構造BigDecimal對象,因此避免了精度問題。所以大家要盡量要使用字符串而不是浮點數(shù)去構造BigDecimal對象,如果實在不行,就使用BigDecimal#valueOf()方法吧。
1.2 等值比較
BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("1.00");
System.out.println(bd1.equals(bd2));
System.out.println(bd1.compareTo(bd2));
控制臺的輸出將會是:
false
0
究其原因是,BigDecimal中equals方法的實現(xiàn)會比較兩個數(shù)字的精度,而compareTo方法則只會比較數(shù)值的大小。
1.3 BigDecimal并不代表無限精度
先看這段代碼
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
a.divide(b) // results in the following exception.
結果會拋出異常:
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
關于這個異常,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.
大意是,如果除法的商的結果是一個無限小數(shù)但是我們期望返回精確的結果,那程序就會拋出異常?;氐轿覀兊倪@個例子,我們需要告訴JVM我們不需要返回精確的結果就好了
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
a.divide(b, 2, RoundingMode.HALF_UP)// 0.33
1.4 BigDecimal轉回String要小心
BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
String out = d.toString(); // Or perform any formatting that needs to be done
System.out.println(out); // 1.23345353454567E+16
可以看到結果已經(jīng)被轉換成了科學計數(shù)法,可能這個并不是預期的結果BigDecimal有三個方法可以轉為相應的字符串類型,切記不要用錯:
String toString(); // 有必要時使用科學計數(shù)法
String toPlainString(); // 不使用科學計數(shù)法
String toEngineeringString(); // 工程計算中經(jīng)常使用的記錄數(shù)字的方法,與科學計數(shù)法類似,但要求10的冪必須是3的倍數(shù)
1.5 執(zhí)行順序不能調換(乘法交換律失效)
乘法滿足交換律是一個常識,但是在計算機的世界里,會出現(xiàn)不滿足乘法交換律的情況
BigDecimal a = BigDecimal.valueOf(1.0);
BigDecimal b = BigDecimal.valueOf(3.0);
BigDecimal c = BigDecimal.valueOf(3.0);
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP).multiply(c)); // 0.990
System.out.println(a.multiply(c).divide(b, 2, RoundingMode.HALF_UP)); // 1.00
別小看這這0.01的差別,在匯金領域,會產(chǎn)生非常大的金額差異。
2. 最佳實踐
關于金額計算,很多業(yè)務團隊會基于BigDecimal再封裝一個Money類,其實我們直接可以用一個半官方的Money類:JSR 354 ,雖然沒能在Java 9中成為Java標準,很有可能集成到后續(xù)的Java版本中成為官方庫。
2.1 maven坐標
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>1.1</version>
</dependency>
2.2 新建Money類
CurrencyUnit cny = Monetary.getCurrency("CNY");
Money money = Money.of(1.0, cny);
// 或者 Money money = Money.of(1.0, "CNY");
//System.out.println(money);
2.3 金額運算
CurrencyUnit cny = Monetary.getCurrency("CNY");
Money oneYuan = Money.of(1.0, cny);
Money threeYuan = oneYuan.add(Money.of(2.0, "CNY")); //CNY 3
Money tenYuan = oneYuan.multiply(10); // CNY 10
Money fiveFen = oneYuan.divide(2); //CNY 0.5
2.4 比較相等
Money fiveFen = Money.of(0.5, "CNY"); //CNY 0.5
Money anotherFiveFen = Money.of(0.50, "CNY"); // CNY 0.50
System.out.println(fiveFen.equals(anotherFiveFen)); // true
可以看到,這個類對金額做了顯性的抽象,增加了金額的單位,也避免了直接使用BigDecimal的一些坑。
完
我的新書《深入理解Java核心技術》已經(jīng)上市了,上市后一直蟬聯(lián)京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~
長按掃碼享受6折優(yōu)惠
往期推薦

B站被罵上了熱搜。。

為什么都想去國企?技術落后,薪資低,進去以后躺平幾年,出來都找不到工作!

歡迎大家關注我的抖音~
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
