BigDecimal的精度與刻度

BigDecimal是Java中用于高精度算術(shù)運(yùn)算的類(lèi)。當(dāng)您需要精確地處理非常大或非常小的數(shù)字時(shí),例如在金融計(jì)算中,它特別有用。由于眾所周知得原因,Double這種類(lèi)型在某些情況下會(huì)出現(xiàn)丟失精度的問(wèn)題,所以在需要對(duì)較為敏感的數(shù)據(jù)(比如與金額有關(guān)的)進(jìn)行運(yùn)算時(shí),我們都會(huì)用BigDecimal。但是,用BigDecimal不代表就一定沒(méi)問(wèn)題,我們今天就討論一下關(guān)于BigDecimal的問(wèn)題。
精度與刻度
要正確使用BigDecimal,首先要清楚精度(precision)和刻度(scale)的概念。
-
Precision(精度):表示數(shù)值的總位數(shù),包括小數(shù)點(diǎn)前后的位數(shù)。例如,數(shù)值 123.45 的精度是 5,因?yàn)樗?5 位數(shù)字。
-
Scale(刻度):表示小數(shù)點(diǎn)后的位數(shù)。例如,數(shù)值 123.45 的刻度是 2,因?yàn)樾?shù)點(diǎn)后有 2 位數(shù)字。
舉個(gè)例子,如果一個(gè)數(shù)值類(lèi)型定義為 DECIMAL(7, 2),那么它的精度是 7,刻度是 2。這意味著這個(gè)數(shù)值最多可以有 7 位數(shù)字,其中 2 位在小數(shù)點(diǎn)后,5 位在小數(shù)點(diǎn)前。
(p.s. DECIMAL這個(gè)數(shù)值類(lèi)型通常是用在數(shù)據(jù)庫(kù)中的,JAVA中并沒(méi)有這個(gè)類(lèi)型。用這個(gè)例子是因?yàn)樗梢宰钋逦卣f(shuō)明精度與刻度)
BigDecimal類(lèi)中也有獲取精度和刻度的方法
BigDecimal num = new BigDecimal("12.1234");
System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale()));
//輸出:precision:6 scale:4
除法中的刻度
在用BigDecimal做除法運(yùn)算,使用divide方法的時(shí)候,可以指定刻度,也可以不指定。
當(dāng)指定刻度,即保留幾位小數(shù)的時(shí)候,需要指定進(jìn)位模式(RoundingMode)。
可選的模式有UP、DOWN、CEILING、FLOOR、HALF_UP、HALF_DOWN、HALF_EVEN、UNNECESSARY。
JDK api中用一個(gè)表格比較了這幾種模式的區(qū)別
Result of rounding input to one digit with the given rounding mode
| Input Number | UP | DOWN | CEILING | FLOOR | HALF_UP | HALF_DOWN | HALF_EVEN | UNNECESSARY |
|---|---|---|---|---|---|---|---|---|
| 5.5 | 6 | 5 | 6 | 5 | 6 | 5 | 6 | throw ArithmeticException |
| 2.5 | 3 | 2 | 3 | 2 | 3 | 2 | 2 | throw ArithmeticException |
| 1.6 | 2 | 1 | 2 | 1 | 2 | 2 | 2 | throw ArithmeticException |
| 1.1 | 2 | 1 | 2 | 1 | 1 | 1 | 1 | throw ArithmeticException |
| 1.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| -1.0 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
| -1.1 | -2 | -1 | -1 | -2 | -1 | -1 | -1 | throw ArithmeticException |
| -1.6 | -2 | -1 | -1 | -2 | -2 | -2 | -2 | throw ArithmeticException |
| -2.5 | -3 | -2 | -2 | -3 | -3 | -2 | -2 | throw ArithmeticException |
| -5.5 | -6 | -5 | -5 | -6 | -6 | -5 | -6 | throw ArithmeticException |
按指定的規(guī)則進(jìn)位,保留幾位小數(shù),這沒(méi)有問(wèn)題。
如果不指定刻度呢?
BigDecimal one = new BigDecimal("1");
BigDecimal eight = new BigDecimal("8");
System.out.println(one.divide(eight));//輸出 0.125
BigDecimal three = new BigDecimal("3");
System.out.println(one.divide(three));// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
當(dāng)結(jié)果能除盡的時(shí)候正常處理,當(dāng)除不盡即結(jié)果是無(wú)限循環(huán)小數(shù)的時(shí)候,程序拋出異常。
看一下源碼:
public BigDecimal divide(BigDecimal divisor) {
/*
* Handle zero cases first.
*/
if (divisor.signum() == 0) { // x/0
if (this.signum() == 0) // 0/0
throw new ArithmeticException("Division undefined"); // NaN
throw new ArithmeticException("Division by zero");
}
// Calculate preferred scale
int preferredScale = saturateLong((long) this.scale - divisor.scale);
if (this.signum() == 0) // 0/y
return zeroValueOf(preferredScale);
else {
/*
* If the quotient this/divisor has a terminating decimal
* expansion, the expansion can have no more than
* (a.precision() + ceil(10*b.precision)/3) digits.
* Therefore, create a MathContext object with this
* precision and do a divide with the UNNECESSARY rounding
* mode.
*/
MathContext mc = new MathContext( (int)Math.min(this.precision() +
(long)Math.ceil(10.0*divisor.precision()/3.0),
Integer.MAX_VALUE),
RoundingMode.UNNECESSARY);
BigDecimal quotient;
try {
quotient = this.divide(divisor, mc);
} catch (ArithmeticException e) {
throw new ArithmeticException("Non-terminating decimal expansion; " +
"no exact representable decimal result.");
}
int quotientScale = quotient.scale();
// divide(BigDecimal, mc) tries to adjust the quotient to
// the desired one by removing trailing zeros; since the
// exact divide method does not have an explicit digit
// limit, we can add zeros too.
if (preferredScale > quotientScale)
return quotient.setScale(preferredScale, ROUND_UNNECESSARY);
return quotient;
}
}
MathContext mc = new MathContext( (int)Math.min(this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0),Integer.MAX_VALUE),RoundingMode.UNNECESSARY);
這一句算了一個(gè)刻度,然后進(jìn)位方式選了UNNECESSARY。所以當(dāng)除不盡的時(shí)候就拋異常了。
也就是說(shuō)它斷定,如果一個(gè)除法能除盡,它的小數(shù)位小于this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0)
這句話(huà)的意思是,如果 this 除以 divisor 的商是一個(gè)有限小數(shù),那么這個(gè)小數(shù)的位數(shù)不會(huì)超過(guò)以下公式計(jì)算的結(jié)果:
\(a.precision() + \lceil \frac{10 \cdot b.precision}{3} \rceil\)
其中:
? a.precision() 是 this 的精度,即 this 的總位數(shù)。
? b.precision() 是 divisor 的精度,即 divisor 的總位數(shù)。
? ceil 是向上取整函數(shù)。
這個(gè)公式的作用是估算有限小數(shù)的最大位數(shù)。具體來(lái)說(shuō),它考慮了 this 和 divisor 的精度,并通過(guò)一個(gè)比例關(guān)系來(lái)確定商的小數(shù)位數(shù)上限。
舉個(gè)例子,如果 this 的精度是 5,divisor 的精度是 3,那么公式計(jì)算如下:
\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)
因此,商的有限小數(shù)位數(shù)不會(huì)超過(guò) 15 位。
插一句啊,這是為什么?
AI是這樣回答的:
這個(gè)公式的提出者并沒(méi)有明確的記錄,但它基于數(shù)論中的一些基本原理。證明這個(gè)公式需要用到基本算術(shù)定理(Fundamental Theorem of Arithmetic),即每個(gè)整數(shù)都可以唯一地分解為素?cái)?shù)的乘積。
證明思路
- 有限小數(shù)的條件:
一個(gè)分?jǐn)?shù) \(\frac{a}{b}\)只有在其分母 (b) 僅包含 2 和 5 作為素因子時(shí),才會(huì)有一個(gè)有限小數(shù)。這是因?yàn)?10 的唯一素因子是 2 和 5。- 小數(shù)位數(shù)的估算:
假設(shè)我們有一個(gè)分?jǐn)?shù) \(\frac{a}{b}\),其中 (a) 和 (b) 是整數(shù),并且 (b) 僅包含 2 和 5 作為素因子。我們可以將 (b) 表示為 \((2^m \times 5^n)\),其中 (m) 和 (n) 是非負(fù)整數(shù)。- 小數(shù)位數(shù)的上限:
為了使分?jǐn)?shù) \(\frac{a}{b}\) 變成一個(gè)整數(shù),我們需要將其乘以 \((10^{\max(m, n)})\)。因此,有限小數(shù)的最大位數(shù)可以通過(guò)以下公式估算:
\(a.precision + \lceil \frac{10 \cdot b.precision}{3} \rceil\)
其中 (a.precision) 是分子 (a) 的位數(shù),(b.precision) 是分母 (b) 的位數(shù)。
具體例子
假設(shè)我們有兩個(gè)數(shù):
? this = 12345(精度為 5,因?yàn)橛?5 位數(shù)字)
? divisor = 678(精度為 3,因?yàn)橛?3 位數(shù)字)
我們想知道 12345 除以 678 的商,如果是有限小數(shù),它的小數(shù)部分最多有多少位。根據(jù)公式:
\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)
因此,12345 除以 678 的商,如果是有限小數(shù),小數(shù)部分最多有 15 位。
額...是數(shù)論啊?那我走,打擾了,打擾了...

為什么你應(yīng)該用字符串來(lái)構(gòu)造BigDecimal
聰明的你應(yīng)該早就發(fā)現(xiàn)了,BigDecimal的構(gòu)造方法有很多個(gè)。應(yīng)該用哪個(gè)呢?很多人都知道應(yīng)該用字符串,可是為什么呢?
因?yàn)椋?dāng)你不用字符串的時(shí)候,會(huì)用很多意想不到的驚喜。
BigDecimal strnum = new BigDecimal("12.1234");
System.out.println(String.format("precision:%s scale:%s", strnum.precision(),strnum.scale()));//輸出 precision:6 scale:4
BigDecimal num = new BigDecimal(12.1234);
System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale()));//輸出precision:50 scale:48
下面一個(gè)刻度是48,這是什么鬼?
再試試除法
BigDecimal one = new BigDecimal("0.1");
BigDecimal eight = new BigDecimal("8");
System.out.println(one.divide(eight));//輸出 0.0125
BigDecimal _1 = new BigDecimal(0.1);
BigDecimal _8 = new BigDecimal(8);
System.out.println(_1.divide(_8));//輸出 0.0125000000000000006938893903907228377647697925567626953125
為什么會(huì)出現(xiàn)這種結(jié)果?這個(gè)原因眾所周知:
二進(jìn)制(也稱(chēng)為基數(shù)2)不能精確表示某些十進(jìn)制數(shù),尤其是那些在十進(jìn)制中有有限小數(shù)位但在二進(jìn)制中需要無(wú)限小數(shù)位的數(shù)。例如,0.1 和 0.2 在二進(jìn)制中無(wú)法精確表示。
這是因?yàn)槎M(jìn)制系統(tǒng)只能使用 0 和 1 來(lái)表示數(shù)值,而某些十進(jìn)制數(shù)在轉(zhuǎn)換為二進(jìn)制時(shí)會(huì)變成無(wú)限循環(huán)小數(shù)。例如:
? 0.1 在二進(jìn)制中表示為 0.00011001100110011...(無(wú)限循環(huán))
? 0.2 在二進(jìn)制中表示為 0.0011001100110011...(無(wú)限循環(huán))
由于計(jì)算機(jī)的存儲(chǔ)空間有限,這些無(wú)限循環(huán)小數(shù)只能被截?cái)啵瑥亩鴮?dǎo)致精度損失。這就是為什么在使用浮點(diǎn)數(shù)進(jìn)行計(jì)算時(shí),可能會(huì)出現(xiàn)精度問(wèn)題。
如果你看源碼你會(huì)發(fā)現(xiàn) public BigDecimal(double val) 和 public BigDecimal(String val)的實(shí)現(xiàn)完全不同。或許你也會(huì)想為什么呢?為什么要實(shí)現(xiàn)兩套不同的呢?你就直接這樣:
public BigDecimal(double val) {
this(String.valueOf(val));
}
不就完了?
至于JDK團(tuán)隊(duì)實(shí)現(xiàn)兩套的原因是什么?你知道嗎?歡迎留言告訴我:)
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
原文: https://wangxuan.me/tech/2024/07/16/the-precision-and-scale-of-BigDecimal.html
浙公網(wǎng)安備 33010602011771號(hào)