當前位置: 首頁>>技術教程>>正文


Java BigDecimal浮點數運算–如何保證運算精度不溢出

加減乘除四則運算是高級程序設計語言(不論機器語言、匯編還是其他高級語言)最基礎的部分,Java作為最流行的軟件開發語言之一,涉及四則運算的程序代碼和功能業務隨處可見。在筆者從事的基於Java語言銀行、電商等軟件係統開發過程中,涉及數字和金錢的業務比較多,也遇到了各種數據精度問題,下麵從幾個小例子,看看如何正確使用Java中的數據類型合理進行四則運算,保證數據精度。

1.整型計算

可能有人會說,整型數據肯定不會有計算錯誤和精度丟失問題。確實是這樣嗎?
首先請看代碼:

int a = 345679;
int b = 8989;
int c = a*b;    // c的結果:-1187658765  顯然是錯誤的

是不是和大家想的不一樣,那麽接著看下麵的代碼:

long a = 345679;
int b = 8989;
long c = a*b;   // c的結果為:3107308531  正確

當然,這種問題對於理解了整型int、long的數據長度限製的程序員來說,再簡單不過了。這裏隻是舉個例子,說明在程序設計語言中,整型數據類型的長度是有位數限製的,再使用四則運算時是避免數據類型越界的問題,特別是加法、乘法運算。

2.Float/double浮點數計算

說到浮點類型,很多人會想到,float類型在做乘法、除法運算時會導致精度溢出。舉例如下:

float a = 1.19f;
float b = 0.93f;
float c = a * b;   //c的結果為:1.1067001 誤差 0.0000001
float aa = 2.94f;
float bb = 0.7f;
float d = aa / bb;  // c的結果為:4.2000003 誤差0.000003

很明顯,float類型數據乘除法確實會導致精度異常,所以大家在高精度要求的業務中,盡量不要使用float類型進行乘除運算,特別是涉及到money等敏感數據時。

那麽float僅僅隻有乘法與除法有誤差嗎?接下看一下例子:

float a = 1.89f;
float b = 1.61f;
float c = a - b;    //c的結果為:0.27999997  誤差0.00000003
float aa = 3.891f;
float bb = 1.19f;
float cc = aa + bb; //cc的結果:5.0810003  誤差0.0000003

類似地,更高精度類型double和float一樣,在加減乘除運算上同樣存在精度丟失異常問題,客觀上理論上來說,float/double浮點數對要求很高精度計算中是不適用的。Java語言中最適合高精度計算的類型是什麽呢,根據筆者多年的經驗與遇到的精度異常問題,java.math.BigDecimal是首選,可以處理任意精度的加減乘除四則運算。

3.BigDecimal浮點計算

BigDecimal官方文檔(摘錄如下):

1. Immutable, arbitrary-precision signed decimal numbers.
2. For all arithmetic operators , the operation is carried out as though an exact intermediate result were first calculated and then rounded to the number of digits specified by the precision setting (if necessary), using the selected rounding mode.
Besides a logical exact result, each arithmetic operation has a preferred scale for representing a result. The preferred scale for each operation is listed in the table below.
3.These scales are the ones used by the methods which return exact arithmetic results; except that an exact divide may have to use a larger scale since the exact result may have more digits.

  1. BigDecimal是不可變的,可表示任意精度的有符號的十進製數字。

  2. 對所有的運算,BigDecimal計算操作會先返回一個精確的中間結果,然後根據設置的精度要求進行數據精度取舍(比如四舍五入),否則默認使用首選精度。除了一個邏輯精確的結果,加減乘除每種運算操作都具有表示結果的首選精度範圍。(加減乘除首選精度以下圖表):

Operation Preferred Scale of Result
Add(加) max(addend.scale(), augend.scale())
Subtract(減) max(minuend.scale(), subtrahend.scale())
Multiply(乘) multiplier.scale() + multiplicand.scale()
Divide(除) dividend.scale() – divisor.scale()
  1. 加減乘除運算方法都基於這些首選精度返回運算結果,除非精確的除法運算必須使用更大的精度,因為除法運算結果可能有更多的為位數。比如1/32等於0.03125。

既然BigDecimal可以準確保留運算精度,那麽接著用實例驗證,是否BigDecimal運算就不會出現精度異常。

3.1 float運算:

float a1 = 1.6f;
float b1 = 2.7f;
BigDecimal a = new BigDecimal(a1);
BigDecimal b = new BigDecimal(b1);
BigDecimal c = a.multiply(b);// c的結果:4.320000140666962806790252216160297393798828125 
BigDecimal d = a.add(b);  // d的結果:4.30000007152557373046875

3.2 double運算:

double a1 = 1.6;
double b1 = 2.7;
BigDecimal a = new BigDecimal(a1);
BigDecimal b = new BigDecimal(b1);
BigDecimal c = a.multiply(b); //c的結果:4.32000000000000052402526762307390285717225175910329573457130565572459346412870218046009540557861328125
BigDecimal d = a.add(b);  //d的結果:4.300000000000000266453525910037569701671600341796875

從以上數據結果可以看出,float、double雖然經過BigDecimal封裝,但是運算結果同樣存在誤差,那麽怎麽才能保證運算的準確行呢?通過一些實際使用與試驗驗證的經驗,推薦方法是使用BigDecimal構造函數BigDecimal(String),將基本數據類型float/double型數據轉成String,然後通過String創建BigDecimal實例進行運算。針對此構造函數,官方說明如下:

Note: For values other than float and double NaN and ±Infinity, this constructor is compatible with the values returned by Float.toString(float) and Double.toString(double). This is generally the preferred way to convert a float or double into a BigDecimal, as it doesn’t suffer from the unpredictability of the BigDecimal(double) constructor.

使用BigDecimal(String)構造函數將float/double轉換為BigDecimal,這樣做也不會遇到類似BigDecimal(double)構造函數的不可預測性。
同樣地,繼續來用例子證實BigDecimal(String)的運算準確性,為了對比,使用3.1、3.2相同的數據

3.3 更精確的float運算

float a1 = 1.6f;
float b1 = 2.7f;
BigDecimal a = new BigDecimal(String.valueOf(a1));   // 構造函數參數為String
BigDecimal b = new BigDecimal(String.valueOf(b1));
BigDecimal c = a.multiply(b);  // c的結果:4.32
BigDecimal d = a.add(b);   // d的結果:4.3

3.4 更精確的double運算

double a1 = 1.6;
double b1 = 2.7;
BigDecimal a = new BigDecimal(String.valueOf(a1));
BigDecimal b = new BigDecimal(String.valueOf(b1));
BigDecimal c = a.multiply(b);   // c的結果:4.32 
BigDecimal d = a.add(b);  // d的結果:4.3

最後,經過前後幾組數據結果對比,進一步證實了BigDecimal(String)能更好的保證加減乘除的運算結果的準確性。

本文由《純淨天空》出品。文章地址: https://vimsky.com/zh-tw/article/3417.html,轉載請注明來源鏈接。