標籤:2.0 問題: 4.0 知識 相等 數字 例子 copy 小數點
java用double和float進行小數計算精度不準確
大多數情況下,使用double和float計算的結果是準確的,但是在一些精度要求很高的系統中或者已知的小數計算得到的結果會不準確,這種問題是非常嚴重的。
《Effective Java》中提到一個原則,那就是float和double只能用來作科學計算或者是工程計算,但在商業計算中我們要用java.math.BigDecimal,通過使用BigDecimal類可以解決上述問題,java的設計者給編程人員提供了一個很有用的類BigDecimal,他可以完善float和double類無法進行精確計算的缺憾。
使用BigDecimal,但一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)來構造(也不能將float或double型轉換成String再來使用BigDecimal(String)來構造,因為在將float或double轉換成String時精度已丟失)。例如new BigDecimal(0.1),它將返回一個BigDecimal,也即0.1000000000000000055511151231257827021181583404541015625,正確使用BigDecimal,程式就可以列印出我們所期望的結果0.9:
Java代碼
System.out.println(new BigDecimal("2.0").subtract(new BigDecimal("1.10")));// 0.9
另外,如果要比較兩個浮點數的大小,要使用BigDecimal的compareTo方法。
執行個體代碼如下:
package ex;import java.math.*;public class BigDecimalDemo { public static void main(String[] args){ System.out.println(ArithUtil.add(0.01, 0.05)); System.out.println(ArithUtil.sub(1.0, 0.42)); System.out.println(ArithUtil.mul(4.015, 100)); System.out.println(ArithUtil.div(123.3, 100)); }}class ArithUtil{ private static final int DEF_DIV_SCALE=10; private ArithUtil(){} //相加 public static double add(double d1,double d2){ BigDecimal b1=new BigDecimal(Double.toString(d1)); BigDecimal b2=new BigDecimal(Double.toString(d2)); return b1.add(b2).doubleValue(); } //相減 public static double sub(double d1,double d2){ BigDecimal b1=new BigDecimal(Double.toString(d1)); BigDecimal b2=new BigDecimal(Double.toString(d2)); return b1.subtract(b2).doubleValue(); } //相乘 public static double mul(double d1,double d2){ BigDecimal b1=new BigDecimal(Double.toString(d1)); BigDecimal b2=new BigDecimal(Double.toString(d2)); return b1.multiply(b2).doubleValue(); } //相除 public static double div(double d1,double d2){ return div(d1,d2,DEF_DIV_SCALE); } public static double div(double d1,double d2,int scale){ if(scale<0){ throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b1=new BigDecimal(Double.toString(d1)); BigDecimal b2=new BigDecimal(Double.toString(d2)); return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue(); }}
現在我們就詳細剖析一下浮點型運算為什麼會造成精度丟失?
1、小數的二進位表示問題
首先我們要搞清楚下面兩個問題: (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。這也就解釋了為什麼浮點型減法出現了"減不盡"的精度丟失問題。
2、 float型在記憶體中的儲存
眾所周知、 Java 的float型在記憶體中佔4個位元組。float的32個二進位位結構如下float記憶體儲存結構 4bytes 31 30 29----23 22----0 表示 實數符號位 指數符號位 指數位 有效數位 其中符號位1表示正,0表示負。有效位元位24位,其中一位是實數符號位。 將一個float型轉化為記憶體儲存格式的步驟為: (1)先將這個實數的絕對值化為二進位格式,注意實數的整數部分和小數部分的二進位方法在上面已經探討過了。 (2)將這個二進位格式實數的小數點左移或右移n位,直到小數點移動到第一個有效數位右邊。 (3)從小數點右邊第一位開始數出二十三位元字放入第22到第0位。 (4)如果實數是正的,則在第31位放入“0”,否則放入“1”。 (5)如果n 是左移得到的,說明指數是正的,第30位放入“1”。如果n是右移得到的或n=0,則第30位放入“0”。 (6)如果n是左移得到的,則將n減去1後化為二進位,並在左邊加“0”補足七位,放入第29到第23位。如果n是右移得到的或n=0,則將n化為二進位後在左邊加“0”補足七位,再各位求反,再放入第29到第23位。 舉例說明: 11.9的記憶體儲存格式 (1) 將11.9化為二進位後大約是" 1011. 1110011001100110011001100..."。 (2) 將小數點左移三位到第一個有效位右側: "1. 011 11100110011001100110 "。 保證有效位元24位,右側多餘的截取(誤差在這裡產生了 )。 (3) 這已經有了二十四位有效數字,將最左邊一位“1”去掉,得到“ 011 11100110011001100110 ”共23bit。將它放入float儲存結構的第22到第0位。 (4) 因為11.9是正數,因此在第31位實數符號位放入“0”。 (5) 由於我們把小數點左移,因此在第30位指數符號位放入“1”。 (6) 因為我們是把小數點左移3位,因此將3減去1得2,化為二進位,並補足7位得到0000010,放入第29到第23位。 最後表示11.9為: 0 1 0000010 011 11100110011001100110 再舉一個例子:0.2356的記憶體儲存格式 (1)將0.2356化為二進位後大約是0.00111100010100000100100000。 (2)將小數點右移三位得到1.11100010100000100100000。 (3)從小數點右邊數出二十三位有效數字,即11100010100000100100000放入第22到第0位。 (4)由於0.2356是正的,所以在第31位放入“0”。 (5)由於我們把小數點右移了,所以在第30位放入“0”。 (6)因為小數點被右移了3位,所以將3化為二進位,在左邊補“0”補足七位,得到0000011,各位取反,得到1111100,放入第29到第23位。 最後表示0.2356為:0 0 1111100 11100010100000100100000 將一個記憶體儲存的float二進位格式轉化為十進位的步驟: (1)將第22位到第0位的位元寫出來,在最左邊補一位“1”,得到二十四位有效數字。將小數點點在最左邊那個“1”的右邊。 (2)取出第29到第23位所表示的值n。當30位是“0”時將n各位求反。當30位是“1”時將n增1。 (3)將小數點左移n位(當30位是“0”時)或右移n位(當30位是“1”時),得到一個二進位表示的實數。 (4)將這個二進位實數化為十進位,並根據第31位是“0”還是“1”加上正號或負號即可。
3、浮點型的減法運算
浮點加減運算過程比定點運算過程複雜。完成浮點加減運算的操作過程大體分為四步:(1) 0運算元的檢查; 如果判斷兩個需要加減的浮點數有一個為0,即可得知運算結果而沒有必要再進行有序的一些列操作。 (2) 比較階碼(指數位)大小並完成對階; 兩浮點數進行加減,首先要看兩數的 指數位 是否相同,即小數點位置是否對齊。若兩數 指數位 相同,表示小數點是對齊的,就可以進行尾數的加減運算。反之,若兩數階碼不同,表示小數點位置沒有對齊,此時必須使兩數的階碼相同,這個過程叫做對階 。 如何對 階(假設兩浮點數的指數位為 Ex 和 Ey ): 通過尾數的移位以改變 Ex 或 Ey ,使之相等。 由 於浮點表示的數多是規格化的,尾數左移會引起最高有位的丟失,造成很大誤差;而尾數右移雖引起最低有效位的丟失,但造成的誤差較小,因此,對階操作規定使 尾數右移,尾數右移後使階碼作相應增加,其數值保持不變。很顯然,一個增加後的階碼與另一個相等,所增加的階碼一定是小階。因此在對階時,總是使小階向大階看齊 ,即小階的尾數向右移位 ( 相當於小數點左移 ) ,每右移一位,其階碼加 1 ,直到兩數的階碼相等為止,右移的位元等於階差 △ E 。 (3) 尾數(有效數位)進行加或減運算;(4) 結果規格化並進行舍入處理。
java在計算浮點數的時候,由於二進位無法精確表示0.1的值(就好比十進位無法精確表示1/3一樣),所以一般會對小數格式化處理.
但是如果涉及到金錢的項目,一點點誤差都不能有,必須使用精確運算的時候,就可以使用BigDecimal方法計算.
但是在使用中還需要注意一個問題:
//直接使用double類型資料進行運算System.out.println(0.05+0.01);//使用BigDecimal的double參數的構造器BigDecimal bd1 = new BigDecimal(0.05);BigDecimal bd2 = new BigDecimal(0.01);System.out.println(bd1.add(bd2));//使用BigDecimal的String參數的構造器BigDecimal bd3 = new BigDecimal("0.05");BigDecimal bd4 = new BigDecimal("0.01");System.out.println(bd3.add(bd4));
這三個輸出結果是不一樣的:
所以在計算的時候,應該先把數字轉換成String類型的,才能得到最精確的值.
附常用的方法:
add 加
subtract 減
multiply 乘
divide 除
abs 絕對值
getScale 根據一個規則取幾位小數
pow 幾次方
中文API連結:
http://www.apihome.cn/api/java/BigDecimal.html
java用double和float進行小數計算精度不準確