背景
一直從事金融相關(guān)項(xiàng)目,所以對(duì)BigDecimal再熟悉不過(guò)了碌补,也曾看到很多同學(xué)因?yàn)椴恢馈⒉涣私饣蚴褂貌划?dāng)導(dǎo)致資損事件發(fā)生棉饶。
所以厦章,如果你從事金融相關(guān)項(xiàng)目,或者你的項(xiàng)目中涉及到金額的計(jì)算照藻,那么你一定要花時(shí)間看看這篇文章袜啃,全面學(xué)習(xí)一下BigDecimal。
BigDecimal概述
Java在java.math包中提供的API類BigDecimal幸缕,用來(lái)對(duì)超過(guò)16位有效位的數(shù)進(jìn)行精確的運(yùn)算群发。雙精度浮點(diǎn)型變量double可以處理16位有效數(shù)晰韵,但在實(shí)際應(yīng)用中,可能需要對(duì)更大或者更小的數(shù)進(jìn)行運(yùn)算和處理熟妓。
一般情況下雪猪,對(duì)于不需要準(zhǔn)確計(jì)算精度的數(shù)字,可以直接使用Float和Double處理起愈,但是Double.valueOf(String) 和Float.valueOf(String)會(huì)丟失精度只恨。所以如果需要精確計(jì)算的結(jié)果,則必須使用BigDecimal類來(lái)操作告材。
BigDecimal對(duì)象提供了傳統(tǒng)的+坤次、-、*斥赋、/等算術(shù)運(yùn)算符對(duì)應(yīng)的方法,通過(guò)這些方法進(jìn)行相應(yīng)的操作产艾。BigDecimal都是不可變的(immutable)的疤剑, 在進(jìn)行每一次四則運(yùn)算時(shí),都會(huì)產(chǎn)生一個(gè)新的對(duì)象 闷堡,所以在做加減乘除運(yùn)算時(shí)要記得要保存操作后的值隘膘。
BigDecimal的4個(gè)坑
在使用BigDecimal時(shí),有4種使用場(chǎng)景下的坑杠览,你一定要了解一下弯菊,如果使用不當(dāng),必定很慘踱阿。掌握這些案例管钳,當(dāng)別人寫出有坑的代碼,你也能夠一眼識(shí)別出來(lái)软舌,大牛就是這么練成的才漆。
第一:浮點(diǎn)類型的坑
在學(xué)習(xí)了解BigDecimal的坑之前,先來(lái)說(shuō)一個(gè)老生常談的問(wèn)題:如果使用Float佛点、Double等浮點(diǎn)類型進(jìn)行計(jì)算時(shí)醇滥,有可能得到的是一個(gè)近似值,而不是精確的值超营。
比如下面的代碼:
@Test
public void test0(){
float a = 1;
float b = 0.9f;
System.out.println(a - b);
}
復(fù)制代碼
結(jié)果是多少鸳玩?0.1嗎?不是演闭,執(zhí)行上面代碼執(zhí)行的結(jié)果是0.100000024不跟。之所以產(chǎn)生這樣的結(jié)果,是因?yàn)?.1的二進(jìn)制表示是無(wú)限循環(huán)的船响。由于計(jì)算機(jī)的資源是有限的躬拢,所以是沒辦法用二進(jìn)制精確的表示 0.1躲履,只能用「近似值」來(lái)表示,就是在有限的精度情況下聊闯,最大化接近 0.1 的二進(jìn)制數(shù)工猜,于是就會(huì)造成精度缺失的情況。
關(guān)于上述的現(xiàn)象大家都知道菱蔬,不再詳細(xì)展開篷帅。同時(shí),還會(huì)得出結(jié)論在科學(xué)計(jì)數(shù)法時(shí)可考慮使用浮點(diǎn)類型拴泌,但如果是涉及到金額計(jì)算要使用BigDecimal來(lái)計(jì)算魏身。
那么,BigDecimal就一定能避免上述的浮點(diǎn)問(wèn)題嗎蚪腐?來(lái)看下面的示例:
@Test
public void test1(){
BigDecimal a = new BigDecimal(0.01);
BigDecimal b = BigDecimal.valueOf(0.01);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
復(fù)制代碼
上述單元測(cè)試中的代碼箭昵,a和b結(jié)果分別是什么?
a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01
復(fù)制代碼
上面的實(shí)例說(shuō)明回季,即便是使用BigDecimal家制,結(jié)果依舊會(huì)出現(xiàn)精度問(wèn)題。這就涉及到創(chuàng)建BigDecimal對(duì)象時(shí)泡一,如果有初始值颤殴,是采用new BigDecimal的形式,還是通過(guò)BigDecimal#valueOf方法了鼻忠。
之所以會(huì)出現(xiàn)上述現(xiàn)象涵但,是因?yàn)閚ew BigDecimal時(shí),傳入的0.1已經(jīng)是浮點(diǎn)類型了帖蔓,鑒于上面說(shuō)的這個(gè)值只是近似值矮瘟,在使用new BigDecimal時(shí)就把這個(gè)近似值完整的保留下來(lái)了。
而BigDecimal#valueOf則不同讨阻,它的源碼實(shí)現(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));
}
復(fù)制代碼
在valueOf內(nèi)部芥永,使用Double#toString方法,將浮點(diǎn)類型的值轉(zhuǎn)換成了字符串钝吮,因此就不存在精度丟失問(wèn)題了埋涧。
此時(shí)就得出一個(gè)基本的結(jié)論:第一,在使用BigDecimal構(gòu)造函數(shù)時(shí)奇瘦,盡量傳遞字符串而非浮點(diǎn)類型棘催;第二,如果無(wú)法滿足第一條耳标,則可采用BigDecimal#valueOf方法來(lái)構(gòu)造初始化值醇坝。
這里延伸一下,BigDecimal常見的構(gòu)造方法有如下幾種:
BigDecimal(int) 創(chuàng)建一個(gè)具有參數(shù)所指定整數(shù)值的對(duì)象。
BigDecimal(double) 創(chuàng)建一個(gè)具有參數(shù)所指定雙精度值的對(duì)象呼猪。
BigDecimal(long) 創(chuàng)建一個(gè)具有參數(shù)所指定長(zhǎng)整數(shù)值的對(duì)象画畅。
BigDecimal(String) 創(chuàng)建一個(gè)具有參數(shù)所指定以字符串表示的數(shù)值的對(duì)象。
復(fù)制代碼
其中涉及到參數(shù)類型為double的構(gòu)造方法宋距,會(huì)出現(xiàn)上述的問(wèn)題轴踱,使用時(shí)需特別留意。
第二:浮點(diǎn)精度的坑
如果比較兩個(gè)BigDecimal的值是否相等谚赎,你會(huì)如何比較淫僻?使用equals方法還是compareTo方法呢?
先來(lái)看一個(gè)示例:
@Test
public void test2(){
BigDecimal a = new BigDecimal("0.01");
BigDecimal b = new BigDecimal("0.010");
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));
}
復(fù)制代碼
乍一看感覺可能相等壶唤,但實(shí)際上它們的本質(zhì)并不相同雳灵。
equals方法是基于BigDecimal實(shí)現(xiàn)的equals方法來(lái)進(jìn)行比較的,直觀印象就是比較兩個(gè)對(duì)象是否相同闸盔,那么代碼是如何實(shí)現(xiàn)的呢悯辙?
@Override
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());
}
復(fù)制代碼
仔細(xì)閱讀代碼可以看出,equals方法不僅比較了值是否相等迎吵,還比較了精度是否相同笑撞。上述示例中,由于兩者的精度不同钓觉,所以equals方法的結(jié)果當(dāng)然是false了。而compareTo方法實(shí)現(xiàn)了Comparable接口坚踩,真正比較的是值的大小荡灾,返回的值為-1(小于),0(等于)瞬铸,1(大于)批幌。
基本結(jié)論:通常情況,如果比較兩個(gè)BigDecimal值的大小嗓节,采用其實(shí)現(xiàn)的compareTo方法荧缘;如果嚴(yán)格限制精度的比較,那么則可考慮使用equals方法拦宣。
另外截粗,這種場(chǎng)景在比較0值的時(shí)候比較常見,比如比較BigDecimal("0")鸵隧、BigDecimal("0.0")绸罗、BigDecimal("0.00"),此時(shí)一定要使用compareTo方法進(jìn)行比較豆瘫。
第三:設(shè)置精度的坑
在項(xiàng)目中看到好多同學(xué)通過(guò)BigDecimal進(jìn)行計(jì)算時(shí)不設(shè)置計(jì)算結(jié)果的精度和舍入模式珊蟀,真是著急人,雖然大多數(shù)情況下不會(huì)出現(xiàn)什么問(wèn)題外驱。但下面的場(chǎng)景就不一定了:
@Test
public void test3(){
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
a.divide(b);
}
復(fù)制代碼
執(zhí)行上述代碼的結(jié)果是什么育灸?ArithmeticException異常腻窒!
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1690)
...
復(fù)制代碼
這個(gè)異常的發(fā)生在官方文檔中也有說(shuō)明:
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.
總結(jié)一下就是,如果在除法(divide)運(yùn)算過(guò)程中磅崭,如果商是一個(gè)無(wú)限小數(shù)(0.333…)儿子,而操作的結(jié)果預(yù)期是一個(gè)精確的數(shù)字,那么將會(huì)拋出ArithmeticException
異常绽诚。
此時(shí)典徊,只需在使用divide方法時(shí)指定結(jié)果的精度即可:
@Test
public void test3(){
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP);
System.out.println(c);
}
復(fù)制代碼
執(zhí)行上述代碼,輸入結(jié)果為0.33恩够。
基本結(jié)論:在使用BigDecimal進(jìn)行(所有)運(yùn)算時(shí)卒落,一定要明確指定精度和舍入模式。
拓展一下蜂桶,舍入模式定義在RoundingMode枚舉類中儡毕,共有8種:
- RoundingMode.UP:舍入遠(yuǎn)離零的舍入模式。在丟棄非零部分之前始終增加數(shù)字(始終對(duì)非零舍棄部分前面的數(shù)字加1)扑媚。注意腰湾,此舍入模式始終不會(huì)減少計(jì)算值的大小。
- RoundingMode.DOWN:接近零的舍入模式疆股。在丟棄某部分之前始終不增加數(shù)字(從不對(duì)舍棄部分前面的數(shù)字加1费坊,即截短)。注意旬痹,此舍入模式始終不會(huì)增加計(jì)算值的大小附井。
- RoundingMode.CEILING:接近正無(wú)窮大的舍入模式。如果 BigDecimal 為正两残,則舍入行為與 ROUNDUP 相同;如果為負(fù)永毅,則舍入行為與 ROUNDDOWN 相同。注意人弓,此舍入模式始終不會(huì)減少計(jì)算值沼死。
- RoundingMode.FLOOR:接近負(fù)無(wú)窮大的舍入模式。如果 BigDecimal 為正崔赌,則舍入行為與 ROUNDDOWN 相同;如果為負(fù)意蛀,則舍入行為與 ROUNDUP 相同。注意峰鄙,此舍入模式始終不會(huì)增加計(jì)算值浸间。
- RoundingMode.HALF_UP:向“最接近的”數(shù)字舍入,如果與兩個(gè)相鄰數(shù)字的距離相等吟榴,則為向上舍入的舍入模式魁蒜。如果舍棄部分 >= 0.5,則舍入行為與 ROUND_UP 相同;否則舍入行為與 ROUND_DOWN 相同。注意兜看,這是我們?cè)谛W(xué)時(shí)學(xué)過(guò)的舍入模式(四舍五入)锥咸。
- RoundingMode.HALF_DOWN:向“最接近的”數(shù)字舍入,如果與兩個(gè)相鄰數(shù)字的距離相等细移,則為上舍入的舍入模式搏予。如果舍棄部分 > 0.5,則舍入行為與 ROUND_UP 相同;否則舍入行為與 ROUND_DOWN 相同(五舍六入)弧轧。
- RoundingMode.HALF_EVEN:向“最接近的”數(shù)字舍入雪侥,如果與兩個(gè)相鄰數(shù)字的距離相等,則向相鄰的偶數(shù)舍入精绎。如果舍棄部分左邊的數(shù)字為奇數(shù)速缨,則舍入行為與 ROUNDHALFUP 相同;如果為偶數(shù),則舍入行為與 ROUNDHALF_DOWN 相同代乃。注意旬牲,在重復(fù)進(jìn)行一系列計(jì)算時(shí),此舍入模式可以將累加錯(cuò)誤減到最小搁吓。此舍入模式也稱為“銀行家舍入法”原茅,主要在美國(guó)使用。四舍六入堕仔,五分兩種情況擂橘。如果前一位為奇數(shù),則入位摩骨,否則舍去贝室。以下例子為保留小數(shù)點(diǎn)1位,那么這種舍入方式下的結(jié)果仿吞。1.15 ==> 1.2 ,1.25 ==> 1.2
- RoundingMode.UNNECESSARY:斷言請(qǐng)求的操作具有精確的結(jié)果,因此不需要舍入捡偏。如果對(duì)獲得精確結(jié)果的操作指定此舍入模式唤冈,則拋出ArithmeticException。
通常我們使用的四舍五入即RoundingMode.HALF_UP银伟。
第四:三種字符串輸出的坑
當(dāng)使用BigDecimal之后你虹,需要轉(zhuǎn)換成String類型,你是如何操作的彤避?直接toString傅物?
先來(lái)看看下面的代碼:
@Test
public void test4(){
BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
System.out.println(a.toString());
}
復(fù)制代碼
執(zhí)行的結(jié)果是上述對(duì)應(yīng)的值嗎?并不是:
3.563453525545672E+16
復(fù)制代碼
也就是說(shuō)琉预,本來(lái)想打印字符串的董饰,結(jié)果打印出來(lái)的是科學(xué)計(jì)數(shù)法的值。
這里我們需要了解BigDecimal轉(zhuǎn)換字符串的三個(gè)方法
- toPlainString():不使用任何科學(xué)計(jì)數(shù)法;
- toString():在必要的時(shí)候使用科學(xué)計(jì)數(shù)法卒暂;
- toEngineeringString() :在必要的時(shí)候使用工程計(jì)數(shù)法啄栓。類似于科學(xué)計(jì)數(shù)法,只不過(guò)指數(shù)的冪都是3的倍數(shù)也祠,這樣方便工程上的應(yīng)用昙楚,因?yàn)樵诤芏鄦挝晦D(zhuǎn)換的時(shí)候都是10^3;
三種方法展示結(jié)果示例如下:
基本結(jié)論:根據(jù)數(shù)據(jù)結(jié)果展示格式不同诈嘿,采用不同的字符串輸出方法堪旧,通常使用比較多的方法為toPlainString() 。
另外奖亚,NumberFormat類的format()方法可以使用BigDecimal對(duì)象作為其參數(shù)淳梦,可以利用BigDecimal對(duì)超出16位有效數(shù)字的貨幣值,百分值遂蛀,以及一般數(shù)值進(jìn)行格式化控制谭跨。
使用示例如下:
NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立貨幣格式化引用
NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小數(shù)點(diǎn)最多3位
BigDecimal loanAmount = new BigDecimal("15000.48"); //金額
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘
System.out.println("金額:\t" + currency.format(loanAmount));
System.out.println("利率:\t" + percent.format(interestRate));
System.out.println("利息:\t" + currency.format(interest));
復(fù)制代碼
輸出結(jié)果如下:
金額: ¥15,000.48
利率: 0.8%
利息: ¥120.00
復(fù)制代碼
小結(jié)
本篇文章介紹了BigDecimal使用中場(chǎng)景的坑,以及基于這些坑我們得出的“最佳實(shí)踐”李滴。雖然某些場(chǎng)景下推薦使用BigDecimal螃宙,它能夠達(dá)到更好的精度,但性能相較于double和float所坯,還是有一定的損失的谆扎,特別在處理龐大,復(fù)雜的運(yùn)算時(shí)尤為明顯芹助。故一般精度的計(jì)算沒必要使用BigDecimal堂湖。而必須使用時(shí),一定要規(guī)避上述的坑状土。