使用BigDecimal實現精確加減乘除運算
商業計算不要使用浮點數
在前幾期的每日一練題目中,有一道題目是,需要精確計算的時候一定要使用BigDecimal。在本科時候其實計算機原理就曾經學過計算機的浮點數表示,但是從小數學的慣性思維上,總覺得乘法就移一下小數點,不會出問題。在商業計算中遇到帶小數點的“元”轉換為“分”的時候,進行*100的操作總覺得double根本不會出問題,於是怒踩了一坑。
簡單理解為什麼會有精度問題
大家都知道計算機要用二進位制表示,基本的十進位制和二進位制的換算應該都清楚。例如:
0.5(十進位制) = 0.1(二進位制),因為2^-1是0.5。
0.25(十進位制) = 0.01(二進位制),因為2^-2是0.25。
那0.2呢,怎麼表示呢,表示不出來啊。那就只能找個最接近的近似值來代表。
所以0.2的二進位制小數是0.0011001100110011.......,也就是(2^-3)+(2^-4)+(2^-7)+(2^-8)+.....
簡單介紹IEEE754表示法
以Java中的double值0.2為例,double大小為32位。
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(0.2)));
執行以上程式碼可以看到double型別0.2在機器中的二進位制表示為:11111111001001100110011001100110011001100110011001100110011010。
需要將其補齊64位,在前面加兩個0:
0011111111001001100110011001100110011001100110011001100110011010。
按照IEEE754表示法,我們將其分為三段:
符號(64位):0,
階碼(63-53位):01111111100,
尾數(51-1位):001100110011001100110011001100110011001100110011010。
System.out.println(Integer.valueOf("01111111100",2));
執行以上程式碼可以看到階碼的十進位制值為1020。當階碼不為0或2047時,需要減去1023這個偏移量,並且尾數前面隱藏了”1.”。因此,調整後的表示為:
符號:0,
階碼:-3(十進位制),
尾數:1.1001100110011001100110011001100110011001100110011010。
最終,按照-1^符號 尾數 2^階碼的計算方式,0.2真正的二進位制值如下所示,和真正的0.2明顯是有偏差的:
0.0011001100110011001100110011001100110011001100110011010
Java中正確使用BigDecimal
在Java中遇到商業計算,可以方便的使用BigDecimal來解決精度問題。但是使用中也存在需要注意的地方。
doubled =0.2; System.out.println(newBigDecimal(d));
執行以上程式碼可以看到,java中表示的0.2的值為:
0.200000000000000011102230246251565404236316680908203125,是有偏差的。
但是雖然用了BigDecimal,為什麼資料還是有問題?這是因為在使用BigDecimal時,不要使用double引數的構造方法,而是要使用String引數的構造方法,或是靜態方法valueOf,才能夠保證資料的準確性。
//兩種正確的使用方式
System.out.println(newBigDecimal("0.2"));
System.out.println(BigDecimal.valueOf(0.2));
BigDecimal類建立的是物件,不能使用傳統的+、-、*、/等算術運算子直接對其進行數學運算,而必須呼叫其對應的方法.方法的引數也必須是BigDecimal型別的物件.
構造BigDecimal 物件常用方法
方法一
BigDecimal BigDecimal(double d);
//不允許使用
方法二
BigDecimal BigDecimal(String s);
//常用,推薦使用
方法三
static BigDecimal valueOf(double d);
//常用,推薦使用
注意:
1. double 引數的構造方法,不允許使用!!!!因為它不能精確的得到相應的值,值會變大;
2. String 構造方法是完全可預知的: 寫入 new BigDecimal("0.1") 將建立一個 BigDecimal,它正好等於預期的0.1; 因此,通常建議優先使用 String 構造方法;
3. 靜態方法 valueOf(double val) 內部實現,仍是將 double 型別轉為 String 型別; 這通常是將 double(或float)轉化為 BigDecimal 的首選方法。