Java浮點數float,bigdecimal和double精確計算的精度誤差問題總結


(轉)Java浮點數float,bigdecimal和double精確計算的精度誤差問題總結

1、float整數計算誤差

案例:會員積分字段采用float類型,導致計算會員積分時,7位整數的數據計算結果出現誤差。

原因:超出float精度范圍,無法精確計算。

float和double的精度是由尾數的位數來決定的。浮點數在內存中是按科學計數法來存儲的,其整數部分始終是一個隱含着的“1”,由於它是不變的,故不能對精度造成影響。

float:2^23 = 8388608,一共七位,這意味着最多能有7位有效數字,但絕對能保證的為6位,也即float的精度為6~7位有效數字;
double:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位。

難道只是位數多大的問題,字段類型換成double就可以解決嗎?對於本案例是這樣,因為都是整數計算,但如果有小數位,就不一定了,見下面案例。

 

2、double小數轉bigdecimal后四舍五入計算有誤差

案例:

double g= 12.35;  

BigDecimal bigG=new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); //期望得到12.4

System.out.println("test G:"+bigG.doubleValue()); 

test G:12.3 

原因:

定義double g= 12.35;  而在計算機中二進制表示可能這是樣:定義了一個g=12.34444444444444449,
new BigDecimal(g)   g還是12.34444444444444449 
new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); 得到12.3
正確的定義方式是使用字符串構造函數:
new BigDecimal("12.35").setScale(1, BigDecimal.ROUND_HALF_UP)

 

3、float和double做四則運算誤差

案例:

public class Test{
    public static void main(String args[]){
        System.out.println(0.05+0.01);
        System.out.println(1.0-0.42);
        System.out.println(4.015*100);
        System.out.println(123.3/100);
    }
}

結果:
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999

原因:

 

那么為什么會出現精度丟失呢?在查閱了一些資料以后,我稍微有了一些頭緒,下面是本人的愚見,僅供參考。

    首先得從計算機本身去討論這個問題。我們知道,計算機並不能識別除了二進制數據以外的任何數據。無論我們使用何種編程語言,在何種編譯環境下工作,都要先 把源程序翻譯成二進制的機器碼后才能被計算機識別。以上面提到的情況為例,我們源程序里的2.4是十進制的,計算機不能直接識別,要先編譯成二進制。但問 題來了,2.4的二進制表示並非是精確的2.4,反而最為接近的二進制表示是2.3999999999999999。原因在於浮點數由兩部分組成:指數和尾數,這點如果知道怎樣進行浮點數的二進制與十進制轉換,應該是不難理解的。如果在這個轉換的過程中,浮點數參與了計算,那么轉換的過程就會變得不可預 知,並且變得不可逆。我們有理由相信,就是在這個過程中,發生了精度的丟失。而至於為什么有些浮點計算會得到准確的結果,應該也是碰巧那個計算的二進制與 十進制之間能夠准確轉換。而當輸出單個浮點型數據的時候,可以正確輸出,如

double d = 2.4;
System.out.println(d);

    輸出的是2.4,而不是2.3999999999999999。也就是說,不進行浮點計算的時候,在十進制里浮點數能正確顯示。這更印證了我以上的想法,即如果浮點數參與了計算,那么浮點數二進制與十進制間的轉換過程就會變得不可預知,並且變得不可逆。

    事實上,浮點數並不適合用於精確計算,而適合進行科學計算。這里有一個小知識:既然float和double型用來表示帶有小數點的數,那為什么我們不稱 它們為“小數”或者“實數”,要叫浮點數呢?因為這些數都以科學計數法的形式存儲。當一個數如50.534,轉換成科學計數法的形式為5.053e1,它 的小數點移動到了一個新的位置(即浮動了)。可見,浮點數本來就是用於科學計算的,用來進行精確計算實在太不合適了。

4、bigdecimal構造函數使用不當帶來異常

案例:

 

BigDecimal其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入。要小心使用 BigDecimal(double) 構造函數,因為如果不了解它,會在計算過程中產生舍入誤差。請使用基於整數或 String 的構造函數。

如果使用 BigDecimal(double) 構造函數不恰當,在傳遞給 JDBC setBigDecimal() 方法時,會造成似乎很奇怪的 JDBC 驅動程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數字 0.01 存儲到小數字段:

<span style="font-family: 'Microsoft YaHei';">PreparedStatement ps =
    connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
  ps.setString(1, "penny");
  ps.setBigDecimal(2, new BigDecimal(0.01));
  ps.executeUpdate();</span>

在執行這段似乎無害的代碼時會拋出一些令人迷惑不解的異常(這取決於具體的 JDBC 驅動程序),因為 0.01 的雙精度近似值會導致大的換算值,這可能會使 JDBC 驅動程序或數據庫感到迷惑。JDBC 驅動程序會產生異常,但可能不會說明代碼實際上錯在哪里,除非意識到二進制浮點數的局限性。相反,使用 BigDecimal("0.01") 或 BigDecimal(1, 2) 構造 BigDecimal 來避免這類問題,因為這兩種方法都可以精確地表示小數。


5、解決浮點數精確計算有誤差的方法

 

在《Effective   Java》這本書中也提到這個原則,float和double只能用來做科學計算或者是工程計算,在商業計算中我們要用java.math.BigDecimal。使用BigDecimal並且一定要用String來夠造。

BigDecimal用哪個構造函數?
BigDecimal(double val) 
BigDecimal(String val)   
上面的API簡要描述相當的明確,而且通常情況下,上面的那一個使用起來要方便一些。我們可能想都不想就用上了,會有什么問題呢?等到出了問題的時候,才發現參數是double的構造方法的詳細說明中有這么一段:
Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances nonwithstanding. 
The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.
原來我們如果需要精確計算,非要用String來夠造BigDecimal不可!

 

6、定點數和浮點數的區別

 

在計算機系統的發展過程中,曾經提出過多種方法表達實數。典型的比如相對於浮點數的定點數(Fixed Point Number)。在這種表達方式中,小數點固定的位於實數所有數字中間的某個位置。貨幣的表達就可以使用這種方式,比如 99.00 或者 00.99 可以用於表達具有四位精度(Precision),小數點后有兩位的貨幣值。由於小數點位置固定,所以可以直接用四位數值來表達相應的數值。SQL 中的 NUMBER 數據類型就是利用定點數來定義的。還有一種提議的表達方式為有理數表達方式,即用兩個整數的比值來表達實數。

定點數表達法的缺點在於其形式過於僵硬,固定的小數點位置決定了固定位數的整數部分和小數部分,不利於同時表達特別大的數或者特別小的數。最終,絕大多數現代的計算機系統采納了所謂的浮點數表達方式。這種表達方式利用科學計數法來表達實數,即用一個尾數(Mantissa ),一個基數(Base),一個指數(Exponent)以及一個表示正負的符號來表達實數。比如 123.45 用十進制科學計數法可以表達為 1.2345 × 102 ,其中 1.2345 為尾數,10 為基數,2 為指數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大范圍的實數。

MySQL中使用浮點數類型和定點數類型來表示小數。浮點數類型包括單精度浮點數(FLOAT型)和雙精度浮點數(DOUBLE型)。定點數類型就是DECIMAL型。MySQL的浮點數類型和定點數類型如下表所示:

類型名稱 字節數 負數的取值范圍 非負數的取值范圍
FLOAT 4 -3.402823466E+38~
-1.175494351E-38
0和1.175494351E-38~
3.402823466E+38
DOUBLE 8 -1.7976931348623157E+308~
-2.2250738585072014E-308
0和2.2250738585072014E-308~
1.7976931348623157E+308
DECIMAL(M,D)或DEC(M,D) M+2 同DOUBLE型 同DOUBLE型

從上表中可以看出,DECIMAL型的取值范圍與DOUBLE相同。但是,DECIMAL的有效取值范圍由M和D決定,而且DECIMAL型的字節數是M+2,也就是說,定點數的存儲空間是根據其精度決定的。

7、bigdecimal比等方法

 

如浮點類型一樣, BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測數值之間是否相等時要小心。 equals() 方法認為,兩個表示同一個數但換算值不同(例如, 100.00 和 100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法會認為這兩個數是相等的,所以在從數值上比較兩個 BigDecimal 值時,應該使用 compareTo() 而不是 equals() 。

另外還有一些情形,任意精度的小數運算仍不能表示精確結果。例如, 1 除以 9 會產生無限循環的小數 .111111... 。出於這個原因,在進行除法運算時, BigDecimal 可以讓您顯式地控制舍入。 movePointLeft() 方法支持 10 的冪次方的精確除法。

與零比較:

int r=big_decimal.compareTo(BigDecimal.Zero); //和0,Zero比較
if(r==0) //等於
if(r==1) //大於
if(r==-1) //小於

 

8、簡化bigdecimal計算的小工具類

如果我們要做一個加法運算,需要先將兩個浮點數轉為String,然后夠造成BigDecimal,在其中一個上調用add方法,傳入另一個作為參數,然后把運算的結果(BigDecimal)再轉換為浮點數。你能夠忍受這么煩瑣的過程嗎?網上提供的工具類Arith來簡化操作。它提供以下靜態方法,包括加減乘除和四舍五入:   

 

public   static   double   add(double   v1,double   v2)   
public   static   double   sub(double   v1,double   v2)   
public   static   double   mul(double   v1,double   v2)   
public   static   double   div(double   v1,double   v2)   
public   static   double   div(double   v1,double   v2,int   scale)   
public   static   double   round(double   v,int   scale)  

  1. import java.math.BigDecimal;    
  2.   
  3. /**  
  4.  * 進行BigDecimal對象的加減乘除,四舍五入等運算的工具類  
  5.  * @author ameyume  
  6.  *  
  7.  */  
  8. public class Arith {    
  9.   
  10.     /**   
  11.     * 由於Java的簡單類型不能夠精確的對浮點數進行運算,這個工具類提供精   
  12.     * 確的浮點數運算,包括加減乘除和四舍五入。   
  13.     */    
  14.     //默認除法運算精度    
  15.     private static final int DEF_DIV_SCALE = 10;    
  16.            
  17.     //這個類不能實例化    
  18.     private Arith(){    
  19.     }    
  20.   
  21.     /**   
  22.      * 提供精確的加法運算。   
  23.      * @param v1 被加數   
  24.      * @param v2 加數   
  25.      * @return 兩個參數的和   
  26.      */    
  27.     public static double add(double v1,double v2){    
  28.         BigDecimal b1 = new BigDecimal(Double.toString(v1));    
  29.         BigDecimal b2 = new BigDecimal(Double.toString(v2));    
  30.         return b1.add(b2).doubleValue();    
  31.     }    
  32.        
  33.     /**   
  34.      * 提供精確的減法運算。   
  35.      * @param v1 被減數   
  36.      * @param v2 減數   
  37.      * @return 兩個參數的差   
  38.      */    
  39.     public static double sub(double v1,double v2){    
  40.         BigDecimal b1 = new BigDecimal(Double.toString(v1));    
  41.         BigDecimal b2 = new BigDecimal(Double.toString(v2));    
  42.         return b1.subtract(b2).doubleValue();    
  43.     }    
  44.        
  45.     /**   
  46.      * 提供精確的乘法運算。   
  47.      * @param v1 被乘數   
  48.      * @param v2 乘數   
  49.      * @return 兩個參數的積   
  50.      */    
  51.     public static double mul(double v1,double v2){    
  52.         BigDecimal b1 = new BigDecimal(Double.toString(v1));    
  53.         BigDecimal b2 = new BigDecimal(Double.toString(v2));    
  54.         return b1.multiply(b2).doubleValue();    
  55.     }    
  56.   
  57.     /**   
  58.      * 提供(相對)精確的除法運算,當發生除不盡的情況時,精確到   
  59.      * 小數點以后10位,以后的數字四舍五入。   
  60.      * @param v1 被除數   
  61.      * @param v2 除數   
  62.      * @return 兩個參數的商   
  63.      */    
  64.     public static double div(double v1,double v2){    
  65.         return div(v1,v2,DEF_DIV_SCALE);    
  66.     }    
  67.   
  68.     /**   
  69.      * 提供(相對)精確的除法運算。當發生除不盡的情況時,由scale參數指   
  70.      * 定精度,以后的數字四舍五入。   
  71.      * @param v1 被除數   
  72.      * @param v2 除數   
  73.      * @param scale 表示表示需要精確到小數點以后幾位。   
  74.      * @return 兩個參數的商   
  75.      */    
  76.     public static double div(double v1,double v2,int scale){    
  77.         if(scale<0){    
  78.             throw new IllegalArgumentException(    
  79.                 "The scale must be a positive integer or zero");    
  80.         }    
  81.         BigDecimal b1 = new BigDecimal(Double.toString(v1));    
  82.         BigDecimal b2 = new BigDecimal(Double.toString(v2));    
  83.         return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();    
  84.     }    
  85.   
  86.     /**   
  87.      * 提供精確的小數位四舍五入處理。   
  88.      * @param v 需要四舍五入的數字   
  89.      * @param scale 小數點后保留幾位   
  90.      * @return 四舍五入后的結果   
  91.      */    
  92.     public static double round(double v,int scale){    
  93.         if(scale<0){    
  94.             throw new IllegalArgumentException(    
  95.                 "The scale must be a positive integer or zero");    
  96.         }    
  97.         BigDecimal b = new BigDecimal(Double.toString(v));    
  98.         BigDecimal one = new BigDecimal("1");    
  99.         return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();    
  100.     }    
  101.        
  102.    /**   
  103.     * 提供精確的類型轉換(Float)   
  104.     * @param v 需要被轉換的數字   
  105.     * @return 返回轉換結果   
  106.     */    
  107.     public static float convertsToFloat(double v){    
  108.         BigDecimal b = new BigDecimal(v);    
  109.         return b.floatValue();    
  110.     }    
  111.        
  112.     /**   
  113.     * 提供精確的類型轉換(Int)不進行四舍五入   
  114.     * @param v 需要被轉換的數字   
  115.     * @return 返回轉換結果   
  116.     */    
  117.     public static int convertsToInt(double v){    
  118.         BigDecimal b = new BigDecimal(v);    
  119.         return b.intValue();    
  120.     }    
  121.   
  122.     /**   
  123.     * 提供精確的類型轉換(Long)   
  124.     * @param v 需要被轉換的數字   
  125.     * @return 返回轉換結果   
  126.     */    
  127.     public static long convertsToLong(double v){    
  128.         BigDecimal b = new BigDecimal(v);    
  129.         return b.longValue();    
  130.     }    
  131.   
  132.     /**   
  133.     * 返回兩個數中大的一個值   
  134.     * @param v1 需要被對比的第一個數   
  135.     * @param v2 需要被對比的第二個數   
  136.     * @return 返回兩個數中大的一個值   
  137.     */    
  138.     public static double returnMax(double v1,double v2){    
  139.         BigDecimal b1 = new BigDecimal(v1);    
  140.         BigDecimal b2 = new BigDecimal(v2);    
  141.         return b1.max(b2).doubleValue();    
  142.     }    
  143.   
  144.     /**   
  145.     * 返回兩個數中小的一個值   
  146.     * @param v1 需要被對比的第一個數   
  147.     * @param v2 需要被對比的第二個數   
  148.     * @return 返回兩個數中小的一個值   
  149.     */    
  150.     public static double returnMin(double v1,double v2){    
  151.         BigDecimal b1 = new BigDecimal(v1);    
  152.         BigDecimal b2 = new BigDecimal(v2);    
  153.         return b1.min(b2).doubleValue();    
  154.     }    
  155.   
  156.     /**   
  157.     * 精確對比兩個數字   
  158.     * @param v1 需要被對比的第一個數   
  159.     * @param v2 需要被對比的第二個數   
  160.     * @return 如果兩個數一樣則返回0,如果第一個數比第二個數大則返回1,反之返回-1   
  161.     */    
  162.         public static int compareTo(double v1,double v2){    
  163.         BigDecimal b1 = new BigDecimal(v1);    
  164.         BigDecimal b2 = new BigDecimal(v2);    
  165.         return b1.compareTo(b2);    
  166.     }   
  167.   
  168. }  

精確運算:

BigDecimal payroll = new BigDecimal("2.232");
BigDecimal finance = new BigDecimal("3.453");
if(payroll.compareTo(new BigDecimal("3500.00"))>0){
System.err.println(payroll.compareTo(new BigDecimal("3500"))>0);
}else{
System.err.println(payroll.compareTo(new BigDecimal("3500"))>0);
}
BigDecimal balance = new BigDecimal("4.56");
BigDecimal total  = payroll.add(finance);
String weString =  payroll.add(finance).add(balance).toString();
// payroll.divide(finance)
// payroll.multiply(finance)
// payroll.subtract(finance)
System.out.println("down="+total.setScale(2,BigDecimal.ROUND_HALF_DOWN)+"\tup="+total.setScale
(2,BigDecimal.ROUND_HALF_UP));

參考:

http://justjavac.iteye.com/blog/1073775

http://www.iteye.com/problems/51604

http://blog.163.com/howl_prowler/blog/static/2661971520114553211964/

http://www.cnblogs.com/wingsless/p/3426108.html

http://zhidao.baidu.com/link?url=2L4pkHgVCXlwEeDM0GRHY2gYUwR9d2JC3knqxvHwdyrrdz_LwK92gVAaIy3hhKEQYdUwNjMLe_RJO3cl8sJvbcAnFK-_rMS4Oy_viystUEe


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM