為什么會出現這個問題呢,就這是java和其它計算機語言都會出現的問題,下面我們分析一下為什么會出現這個問題:
float和double類型主要是為了科學計算和工程計算而設計的。他們執行二進制浮點運算,這是為了在廣泛的數字范圍上提供較為精確的快速近似計算而精心設計的。然而,它們並沒有提供完全精確的結果,所以我們不應該用於精確計算的場合。float和double類型尤其不適合用於貨幣運算,因為要讓一個float或double精確的表示0.1或者10的任何其他負數次方值是不可能的(其實道理很簡單,十進制系統中能不能准確表示出1/3呢?同樣二進制系統也無法准確表示1/10)。
浮點運算很少是精確的,只要是超過精度能表示的范圍就會產生誤差。往往產生誤差不是因為數的大小,而是因為數的精度。因此,產生的結果接近但不等於想要的結果。尤其在使用 float 和 double 作精確運算的時候要特別小心。
現在我們就詳細剖析一下浮點型運算為什么會造成精度丟失?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
首先我們要搞清楚下面兩個問題:
(
1
) 十進制整數如何轉化為二進制數
算法很簡單。舉個例子,
11
表示成二進制數:
11
/
2
=
5
余
1
5
/
2
=
2
余
1
2
/
2
=
1
余
0
1
/
2
=
0
余
1
0
結束
11
二進制表示為(從下往上):
1011
這里提一點:只要遇到除以后的結果為
0
了就結束了,大家想一想,所有的整數除以
2
是不是一定能夠最終得到
0
。換句話說,所有的整數轉變為二進制數的算法會不會無限循環下去呢?絕對不會,整數永遠可以用二進制精確表示 ,但小數就不一定了。
(
2
) 十進制小數如何轉化為二進制數
算法是乘以
2
直到沒有了小數為止。舉個例子,
0.9
表示成二進制數
0.9
*
2
=
1.8
取整數部分
1
0.8
(
1.8
的小數部分)*
2
=
1.6
取整數部分
1
0.6
*
2
=
1.2
取整數部分
1
0.2
*
2
=
0.4
取整數部分
0
0.4
*
2
=
0.8
取整數部分
0
0.8
*
2
=
1.6
取整數部分
1
0.6
*
2
=
1.2
取整數部分
0
.........
0.9
二進制表示為(從上往下):
1100100100100
......
注意:上面的計算過程循環了,也就是說*
2
永遠不可能消滅小數部分,這樣算法將無限下去。很顯然,小數的二進制表示有時是不可能精確的 。其實道理很簡單,十進制系統中能不能准確表示出
1
/
3
呢?同樣二進制系統也無法准確表示
1
/
10
。這也就解釋了為什么浮點型減法出現了
"減不盡"
的精度丟失問題。
|
解決方法
使用BigDecmal,而且需要在構造參數使用String類型。
在《Effective Java》這本書中就給出了一個解決方法。該書中也指出,float和double只能用來做科學計算或者是工程計算,在商業計算等精確計算中,我們要用java.math.BigDecimal。
BigDecimal類有4個構造方法,我們只關心對我們解決浮點型數據進行精確計算有用的方法,即
BigDecimal(double value) // 將double型數據轉換成BigDecimal型數據
思路很簡單,我們先通過BigDecimal(double value)方法,將double型數據轉換成BigDecimal數據,然后就可以正常進行精確計算了。等計算完畢后,我們可以對結果做一些處理,比如 對除不盡的結果可以進行四舍五入。最后,再把結果由BigDecimal型數據轉換回double型數據。
這個思路很正確,但是如果你仔細看看API里關於BigDecimal的詳細說明,你就會知道,如果需要精確計算,我們不能直接用double,而非要用 String來構造BigDecimal不可!所以,我們又開始關心BigDecimal類的另一個方法,即能夠幫助我們正確完成精確計算的 BigDecimal(String value)方法。
// BigDecimal(String value)能夠將String型數據轉換成BigDecimal型數據
那么問題來了,想像一下吧,如果我們要做一個浮點型數據的加法運算,需要先將兩個浮點數轉為String型數據,然后用 BigDecimal(String value)構造成BigDecimal,之后要在其中一個上調用add方法,傳入另一個作為參數,然后把運算的結果(BigDecimal)再轉換為浮 點數。如果每次做浮點型數據的計算都要如此,你能夠忍受這么煩瑣的過程嗎?至少我不能。所以最好的辦法,就是寫一個類,在類中完成這些繁瑣的轉換過程。這 樣,在我們需要進行浮點型數據計算的時候,只要調用這個類就可以了。網上已經有高手為我們提供了一個工具類Arith來完成這些轉換操作。它提供以下靜態 方法,可以完成浮點型數據的加減乘除運算和對其結果進行四舍五入的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
package
com.util;
import
java.math.BigDecimal;
/**
* 由於Java的簡單類型不能夠精確的對浮點數進行運算,這個工具類提供精確的浮點數運算,包括加減乘除和四捨五入。
*/
public
class
Arith {
//默認吃吃飯運算精度
private
static
final
int
DEF_DIV_SCALE =
10
;
//這個類不能實例化
private
Arith() {
}
/**
* 提供精確的加法運算
*
* @param v1
* 被加數
* @param v2
* 加數
* @return 兩個參數的和
*/
public
static
double
add(
double
v1,
double
v2) {
BigDecimal b1 =
new
BigDecimal(Double.toString(v1));
BigDecimal b2 =
new
BigDecimal(Double.toString(v2));
return
b1.add(b2).doubleValue();
}
/**
* 提供精確的減法運算
* @param v1
* 被減數
* @param v2
* 減數
* @return兩個參數的差
*/
public
static
double
sub(
double
v1,
double
v2) {
BigDecimal b1 =
new
BigDecimal(Double.toString(v1));
BigDecimal b2 =
new
BigDecimal(Double.toString(v2));
return
b1.subtract(b2).doubleValue();
}
/**
* 提供精確的乘法運算
*
* @param v1
* 被乘數
* @param v2
* 乘數
* @return 兩個參數的積
*/
public
static
double
mul(
double
v1,
double
v2) {
BigDecimal b1 =
new
BigDecimal(Double.toString(v1));
BigDecimal b2 =
new
BigDecimal(Double.toString(v2));
return
b1.multiply(b2).doubleValue();
}
/**
* 提供(相對)精確的除非運算,當發生除不盡的情況時,精確到小數點以后10位,以后的數字四舍五入
* @param v1
* 被除數
* @param v2
* 除數
* @return 兩個參數的商
*/
public
static
double
div(
double
v1,
double
v2) {
return
div(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相對)精確的除法運算。當發生除不盡的情況時,由scale參數指定精度,以后的數字四舍五入
* @param v1
* 被除數
* @param v2
* 除數
* @param scale
* 表示表示需要精確到小數點以後位數。
* @return 兩個參數的商
*/
public
static
double
div(
double
v1,
double
v2,
int
scale) {
if
(scale <
0
) {
throw
new
IllegalArgumentException(
"The scale must be a positive integer or zero"
);
}
BigDecimal b1 =
new
BigDecimal(Double.toString(v1));
BigDecimal b2 =
new
BigDecimal(Double.toString(v2));
return
b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供精確的小數位四捨五入處理。
* 提供精確的小數位四舍五入處理
*
* @param v
* 需要四捨五入的數位
* @param scale
* 小數點後保留幾位
* @return 四捨五入後的結果
*/
public
static
double
round(
double
v,
int
scale) {
if
(scale <
0
) {
throw
new
IllegalArgumentException(
"The scale must be a positive integer or zero"
);
}
BigDecimal b =
new
BigDecimal(Double.toString(v));
BigDecimal one =
new
BigDecimal(
"1"
);
return
b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
|
附上Arith的源代碼,大家只要把它編譯保存好,要進行浮點數計算的時候,在你的源程序中導入Arith類就可以使用以上靜態方法來進行浮點數的精確計算了。