https://unsplash.com/photos/a-cup-of-coffee-next-to-a-calculator-Uh6-JgCwbfM
有句知名的話是「算錢用浮點,遲早被人扁」。進行財務相關的計算時,在匯率、利率或外幣等項目,經常會遇到小數點。然而在電腦的世界,浮點數的資料型態本身是會有誤差的(與程式語言無關)。因此在精確度要求很高的條件下,我們應避免用 double、float 型態來計算。
本文會先簡述為什麼浮點數不精確,再介紹 Java 的 BigDecimal 及其操作方式。它的運算結果會與我們對數學的認知一致。
此篇轉載自本人的 iThome 鐵人賽文章。
一、浮點數不精準的原因
由於電腦是以二進制的科學記號法來儲存浮點數,所以會造成誤差。
(一)科學記號
國中的數學課有教到「科學記號」,它的好處是能夠將很大或很小的數值,用簡單的方式表達出來。
打個比方,光速大約是每秒 2.9979 億公尺,那麼「2.9979 億」的科學記號寫成「2.9979 × 10 ⁸」,或者表示成「2.9979E+8」。若某細菌的長度是 1.2 微米,則寫成「1.2 × 10 ⁻⁶」或「1.2E-6」。
(二)儲存小數點的方式
以 double 為例,它在記憶體會佔用 64 位元。包含正負符號 1 位元、指數 11 位元與小數 52 位元。
若要儲存十進制 20.5,則將其轉為二進制 10100.1,以科學記號表示成 1.01001 × 2 ¹⁰⁰
若要儲存十進制 20.3,則將其轉為二進制 10100.0 1001 1001 …,以科學記號表示成 1.0 1001 1001… × 2 ¹⁰⁰。
由以上兩個例子能發現,將十進制轉為二進制時,有的數字是可以完整轉換小數(很像整除的感覺)。而像 20.3 是無法完整轉換小數,它出現了「1001」的循環。
討論誤差時,我們關注小數的地方就好。在把小數儲存到記憶體時,若在轉換過程中,52 位元不夠用(如 20.3 發生循環),未轉換完的部份就被捨棄。這就是為什麼浮點數會有誤差的原因。
二、宣告與輸出 BigDecimal
為了在數值運算上能達到精確,在 Java 語言可採用 BigDecimal 類別。該類別的建構子可傳入字串、整數或浮點數。
BigDecimal 可呼叫如 intValue、doubleValue 之類的方法,轉換回基本型態。
另外還有一些像是 intValueExact 的方法,其作用是在 BigDecimal 含有小數,或是超出基本型態的範圍時,會拋出 ArithmeticException。
三、小數進位與捨去
為了方便閱讀後面的除法範例,讓我們先認識 BigDecimal 的小數進位和捨去方式。只要呼叫 setScale 方法,再定義要留到哪一位,並傳入 RoundMode 型態的參數,指定進位或捨去的方式即可。
下面示範一部份 RoundMode 可用的選項。
(一)CEILING
使用 RoundMode.CEILING,可讓數值往正方向進位。
這邊請注意到,BigDecimal 是不可變的(immutable)。因此每次操作後,都會生成新物件,我們要將回傳值給接起來。
(二)FLOOR
使用 RoundMode.FLOOR,可讓數值往負方向捨去。
(三)DOWN
使用 RoundMode.DOWN,可讓數值往 0 的方向捨去。
(四)HALF_UP
使用 HALF_UP,會讓正數與負數,分別往正方向與負方向做四捨五入。也就是絕對值的四捨五入。
四、使用 BigDecimal 運算
(一)加法與減法
使用 add 方法,可進行加法。別忘了要將計算結果給接起來。
使用 subtract 方法,可進行減法。
(二)乘法
使用 multiply 方法,可進行乘法。
從範例中可看到,若參與運算的數值含有小數,即便算完後的小數為 0,BigDecimal 仍會保留小數。呼叫 stripTrailingZeros 方法後便能清除。
(三)除法
使用 divide 方法,可進行除法。由於可能會產生小數,所以必須提供小數的進位或捨去方式,以及到第幾位。
使用 divideAndRemainder 方法,可同時取得商和餘數。
(四)次方
使用 pow 方法,可以計算數值的冪數,也就是 n 次方。
(五)數值比較
BigDecimal 有實作 Comparable 介面,所以可呼叫 compareTo 方法來比較大小。
五、效率比較
稍微比較一下 BigDecimal 與 double 各自的效率。以下的程式,是測量從 0 加到 1 千萬的時間。
在筆者的電腦上跑過後,double 約花費 13 ~ 15 毫秒;而 BigDecimal 約花費 180 ~ 270 毫秒。推測是 BigDecimal 為了達到精確運算,且每次操作都會生成一個新物件,所以需花費較多的效能。
六、例題
以下是計算定期存款本利和的範例程式,假設情境如下:
- 存入金額 50 萬
- 年利率 1.59%
- 存期 1 年
- 每個月複利一次
- 計算利息時,四捨五入到個位數
計算結果為 508008。
若將小數點的處理方式改為「無條件進位」(RoundMode.CEILING)和「無條件捨去」(RoundMode.FLOOR),計算結果分別為 508003 與 508015。
參考資料:
JAVA中浮點型數據的存儲方式
RoundingMode 幾個參數詳解
BigDecimal - 廖雪峰的官方網站
留言
張貼留言