java BigDecimal的使用和四舍五入及格式規范(精准數據)


• Java中的簡單浮點數類型float和double不能夠進行運算。不光是Java,在其它很多編程語言中也有這樣的問題。

如果我們編譯運行下面這個程序會看到什么? 
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 

Java 中的簡單浮點數類型float 和double 不能夠進行運算。不光是Java ,在其它很多編程語言中也有這樣的問題。在大多數情況下,計算的結果是准確的,但是多試幾次(可以做一個循環)就可以試出類似上面的錯誤。現在終於理解為什么要有BCD 碼了。 
這個問題相當嚴重,如果你有9.999999999999 元,你的計算機是不會認為你可以購買10 元的商品的。 
在有的編程語言中提供了專門的貨幣類型來處理這種情況,但是Java 沒有。現在讓我們看看如何解決這個問題。 

四舍五入 
我們的第一個反應是做四舍五入。Math 類中的round 方法不能設置保留幾位小數,我們只能象這樣(保留兩位): 
public   double  round( double  value)  { 
         return  Math.round(value * 100) / 100.0; 
    } 

非常不幸,上面的代碼並不能正常工作,給這個方法傳入4.015 它將返回4.01 而不是4.02 ,如我們在上面看到的 
4.015*100=401.49999999999994 
因此如果我們要做到精確的四舍五入,不能利用簡單類型做任何運算 
java.text.DecimalFormat 也不能解決這個問題: 
System.out.println(new java.text.DecimalFormat("0.00").format(4.025)); 
輸出是4.02 

BigDecimal 
在《Effective Java 》這本書中也提到這個原則,float 和double 只能用來做科學計算或者是工程計算,在商業計算中我們要用java.math.BigDecimal 。BigDecimal 一共有4 個夠造方法,我們不關心用BigInteger 來夠造的那兩個,那么還有兩個,它們是: 

BigDecimal(double val) 
          Translates a double into a BigDecimal. 
BigDecimal(String val) 
          Translates the String repre sentation of a BigDecimal into a BigDecimal. 

上面的API 簡要描述相當的明確,而且通常情況下,上面的那一個使用起來要方便一些。我們可能想都不想就用上了,會有什么問題呢?等到出了問題的時候,才發現上面哪個夠造方法的詳細說明中有這么一段: 
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 不可!在《Effective Java 》一書中的例子是用String 來夠造BigDecimal 的,但是書上卻沒有強調這一點,這也許是一個小小的失誤吧。 

解決方案 
現在我們已經可以解決這個問題了,原則是使用BigDecimal 並且一定要用String 來夠造。 
但是想像一下吧,如果我們要做一個加法運算,需要先將兩個浮點數轉為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) 
附錄 
源文件Arith.java : 
import  java.math.BigDecimal; 

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(); 
    } 


float,double容易產生誤差,對精確度要求比較高時,建議使用decimal來存,decimal在mysql內存是以字符串存儲的,用於定義貨幣要求精確度高的數據。在數據遷移中,float(M,D)是非標准定義,最好不要這樣使用。M為精度,D為標度。 
mysql> create table t1(c1 float(10,2), c3 decimal(10,2)); 
Query OK, 0 rows affected (0.02 sec) 
mysql> insert into t1 values(1234567.23, 1234567.23); 
Query OK, 1 row affected (0.01 sec) 
mysql> select * from t1; 
+------------+------------+ 
| c1 | c3 | 
+------------+------------+ 
| 1234567.25 | 1234567.23 | 
+------------+------------+ 
1 row in set (0.02 sec) 
mysql> insert into t1 values(9876543.21, 9876543.12); 
Query OK, 1 row affected (0.00 sec) 
mysql> 
mysql> select * from t1; 
+------------+------------+ 
| c1 | c3 | 
+------------+------------+ 
| 1234567.25 | 1234567.23 | 
| 9876543.00 | 9876543.12 | 
+------------+------------+ 
2 rows in set (0.00 sec) 
不定義fload, double的精度和標度時,存儲按給出的數值存儲,這於OS和當前的硬件有關。 
decimal默認為decimal(10,0) 
因為誤差問題,在程序中,少用浮點數做=比較,可以做range比較。如果數值比較,最好使用decimal類型。 
精度中,符號不算在內: 
mysql> insert into t1 values(-98765430.21, -98765430.12); 
Query OK, 1 row affected (0.01 sec) 
mysql> select * from t1; 
+--------------+--------------+ 
| c1 | c3 | 
+--------------+--------------+ 
| 1234567.25 | 1234567.23 | 
| 9876543.00 | 9876543.12 | 
| -98765432.00 | -98765430.12 | 
+--------------+--------------+ 
3 rows in set (0.00 sec) 
float占4個字節,double占8個字節,decimail(M,D)占M+2個字節。 

decimal 類型可以精確地表示非常大或非常精確的小數。大至 1028(正或負)以及有效位數多達 28 位的數字可以作為 decimal類型存儲而不失其精確性。該類型對於必須避免舍入錯誤的應用程序(如記賬)很有用。 
float是浮點數,不能指定小數位。 
decimal是精確數,可以指定精度。 
對mysql 5來說 decimal(p,s)中p最大為65,S最大為30 
decimal數據類型最多可存儲 38 個數字,它存儲了一個准確(精確)的數字表達法,不存儲值的近似值。 
當數據值一定要按照指定精確存儲時,可以用帶有小數的decimal數據類型來存儲數字。 
float和real數據類型被稱為近似的數據類型。不存儲精確值.當要求精確的數字狀態時,比如在財務應用程序中,在那些需要舍入的操作中,或在等值核對的操作中,就不使用這些數據類型。這時就要用integer、decimal、money或smallmone數據類型。 
在 WHERE 子句搜索條件中(特別是 = 和 <> 運算符),應避免使用float或real列。最好限制使用float和real列做> 或 < 的比較。 


BigDecimal舍入模式介紹: 
  舍入模式在java.math.RoundingMode 里面: 
RoundingMode.CEILING :向正無限大方向舍入的舍入模式。如果結果為正,則舍入行為類似於 RoundingMode.UP;如果結果為負,則舍入行為類似於 RoundingMode.DOWN。注意,此舍入模式始終不會減少計算值  
輸入數字 使用CEILING舍入模式將數字舍入為一位數 
5.5 6 
2.5 3 
1.1 2 
1.0 1 
-1.0 -1 
-1.1 -1 
-1.6 -1 
-2.5 -2 
-5.5 -5 
RoundingMode.DOWN :向零方向舍入的舍入模式。從不對舍棄部分前面的數字加 1(即截尾)。注意,此舍入模式始終不會增加計算值的絕對值 
輸入數字 使用DOWN舍入模式將數字舍入為一位數 
5.5 5 
2.5 2 
1.1 1 
-1.0 -1 
-1.6 -1 
-2.5 -2 
-5.5 -5 
RoundingMode.FLOOR :向負無限大方向舍入的舍入模式。如果結果為正,則舍入行為類似於 RoundingMode.DOWN;如果結果為負,則舍入行為類似於 RoundingMode.UP。注意,此舍入模式始終不會增加計算值 
輸入數字 使用FLOOR舍入模式將輸入數字舍入為一位 
5.5 5 
2.3 2 
1.6 1 
1.0 1 
-1.1 -2 
-2.5 -3 
-5.5 -6 
RoundingMode.HALF_DOWN :向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向下舍入。如果被舍棄部分 > 0.5,則舍入行為同 RoundingMode.UP;否則舍入行為同 RoundingMode.DOWN  
輸入數字 使用HALF_DOWN輸入模式舍入為一位 
5.5 5 
2.5 2 
1.6 2 
1.0 1 
-1.1 -1 
-1.6 -2 
-2.5 -2 
-5.5 -5 
RoundingMode.HALF_EVEN :向 最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向相鄰的偶數舍入。如果舍棄部分左邊的數字為奇數,則舍入行為同 RoundingMode.HALF_UP;如果為偶數,則舍入行為同 RoundingMode.HALF_DOWN。注意,在重復進行一系列計算時,此舍入模式可以在統計上將累加錯誤減到最小。此舍入模式也稱為“銀行家舍 入法”,主要在美國使用。此舍入模式類似於 Java 中對 float 和 double 算法使用的舍入策略 
輸入數字 使用HALF_EVEN舍入模式將輸入舍為一位 
5.5 6 
2.5 2 
1.6 2 
1.1 1 
-1.0 -1 
-1.6 -2 
-2.5 -2 
-5.5 -6 
RoundingMode.HALF_UP :向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向上舍入。如果被舍棄部分 >= 0.5,則舍入行為同 RoundingMode.UP;否則舍入行為同 RoundingMode.DOWN。注意,此舍入模式就是通常學校里講的四舍五入 
輸入數字 使用HALF_UP舍入模式舍入為一位數 
5.5 6 
2.5 3 
1.6 2 
1.0 1 
-1.1 -1 
-1.6 -2 
-2.5 -3 
-5.5 -6 
RoundingMode.UNNECESSARY :用於斷言請求的操作具有精確結果的舍入模式,因此不需要舍入。如果對生成精確結果的操作指定此舍入模式,則拋出 ArithmeticException 
輸入數字 使用UNNECESSARY模式 
5.5 拋出 ArithmeticException 
2.5 拋出 ArithmeticException 
1.6 拋出 ArithmeticException 
1.0 1 
-1.0 -1.0 
-1.1 拋出 ArithmeticException 
-1.6 拋出 ArithmeticException 
-2.5 拋出 ArithmeticException 
-5.5 拋出 ArithmeticException 
RoundingMode.UP :遠離零方向舍入的舍入模式。始終對非零舍棄部分前面的數字加 1。注意,此舍入模式始終不會減少計算值的絕對值 
輸入數字 使用UP舍入模式將輸入數字舍入為一位數 
5.5 6 
1.6 2 
1.1 2 
1.0 1 
-1.1 -2 
-1.6 -2 
-2.5 -3 
-5.4 -6 
  ——[$] 示例代碼:—— 

import  java.math.BigDecimal; 
import  java.text.DecimalFormat; 
/** 
*使用舍入模式的格式化操作 
**/ 
public class   DoubleFormat { 
    public static void  main(String  args[]){ 
        DoubleFormat format =  new  DoubleFormat(); 
        System.out .println(format.doubleOutPut(12.345, 2)); 
        System.out .println(format.roundNumber(12.335, 2)); 
    } 
    public   String  doubleOutPut(double  v,Integer num){ 
        if ( v == Double.valueOf(v).intValue()){ 
            return  Double.valueOf(v).intValue() +  "" ; 
        }else { 
            BigDecimal b =  new  BigDecimal(Double.toString(v)); 
            return  b.setScale(num,BigDecimal.ROUND_HALF_UP ).toString(); 
        } 
    } 
    public   String  roundNumber(double  v,int  num){ 
        String  fmtString =  "0000000000000000" ;  //16bit 
        fmtString = num>0 ?  "0."   + fmtString.substring(0,num):"0" ; 
        DecimalFormat dFormat =  new  DecimalFormat(fmtString); 
        return  dFormat.format(v); 
    } 

  這段代碼的輸出為: 
12.35 
12.34 


免責聲明!

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



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