關於不能夠精確的對浮點數進行運算的問題


http://edu.eoe.cn/   在線課堂

昨天看到一篇帖子說了幾個很明顯的簡單的浮點的運算,計算機都會算錯。
我引過來給大家看看:‘
運行代碼:

        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

這就很神奇了,0.05+0.01很明顯是0.06嘛,但是為什么會變成0.060000000000000005?
是不是計算器太弱智?真是計算機就不靠譜了?

當然不是的。主要是因為java中的簡單類型並不適用於對浮點的精確計算。不光是JAVA ,其它語言也存在同樣的問題。
雖然現在CPU都支持浮點的運算了,但是CPU在處理的時候,也是先把浮點數(float , double)轉成整數再轉成二進制,然后進行操作,如果有取余,會有不同的取余方式。
再加上運算完成后,再多二進制轉成上層的浮點,又會有一些取舍。就造成了呈現出來時的簡單明顯的錯誤 。

所以說,一般float和double用來做科學計算或者是工程計算,在一般對精度要求較高的地方(如商業),我們會用到BCD碼或者是java.math.BigDecimal。

BSD碼(Binary-Coded Decimal),稱BCD碼或二-十進制代碼,亦稱二進碼十進數。是一種二進制的數字編碼形式,用二進制編碼的十進制代碼。
大家對這個有興趣的可以深入研究一下,今天我們主要講的是BigDecimal類。因為我們在做項目的時候,尤其是對商業項目時,我們不可能還去搞個什么BSD碼,可以直接利用BigDecimal類來完成我們的需求。

BigDecimal 是Java提供的不可變的、任意精度的有符號十進制數。如果想看更多關於BigDecimal的介紹,大家可以自行去查看JDK的文檔。

BigDecimal提供了一系列的構造函數,主用於將double , string等轉化成BigDecimal對象。

BigDecimal(double val) 
        將 double 轉換為 BigDecimal,后者是 double 的二進制浮點值准確的十進制表示形式。
BigDecimal(String val) 
    將 BigDecimal 的字符串表示形式轉換為 BigDecimal。

習慣上我們在使用浮點數的時候都是直接定義的double , float數據類型,在定義上是沒有問題,但是如果我們直接調用BigDecimal(double val) 方法來轉化,那我們可得先注意一下JDK文檔中關於這個構造的詳細說明了:

直接上中文了,英文好的,可以自己去看原版:

注:
此構造方法的結果有一定的不可預知性。有人可能認為在 Java 中寫入 new BigDecimal(0.1) 所創建的 BigDecimal 正好等於 0.1(非標度值 1,其標度為 1),但是它實際上等於 0.1000000000000000055511151231257827021181583404541015625。這是因為 0.1 無法准確地表示為 double(或者說對於該情況,不能表示為任何有限長度的二進制小數)。這樣,傳入 到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。

BigDecimal(double val) 這個方法是不可預知的,所以我們推薦使用BigDecimal(String val) 。
String的構造函數就是可預知的,new BigDecimal(“.1”)如同期望的那樣精確的等於.1。

接下來我們對 0.05+0.01重新修改一下:

BigDecimal bd1 = new BigDecimal(Double.toString(0.05));
                BigDecimal bd2 = new BigDecimal(Double.toString(0.01));
        System.out.println(bd1.add( bd2));

我們再來看看運行的結果:

0.06

這下就是我們想要的結果了,完成了高精度的一個運算了。

注意:
 現在我們已經知道怎么樣來解決這個問題了,原則上是推薦使用BigDecimal(String val) 構造方法。
我建議,在商業的應用中,涉及到money的浮點運算全都定義成String ,在數據庫中保存也是String ,在需要使用到這個money來作運算的時候,我們再把String轉化成BigDecimal來完成高精度的運算。
        
試想一下,如果我們要做一個加法運算,需要先將兩個浮點數轉為String,然后夠造成BigDecimal,在其中一個上調用add方法,傳入另一個作為參數,然后把運算的結果(BigDecimal)再轉換為浮點數。你能夠忍受這么煩瑣的過程嗎?

SO, 網上找了一個比較好的一個工具類,封閉了簡單的操作。可以參考一下:

/**
 * 
 * 由於Java的簡單類型不能夠精確的對浮點數進行運算,這個工具類提供精
 * 確的浮點數運算,包括加減乘除和四舍五入。
 * 
 *
 */
public class Arith{
    //默認除法運算精度
    private static final int DEF_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_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();
    }
}

 首發地址:http://www.eoeandroid.com/thread-230579-1-1.html

另外一個地址:http://krislq.com/150   嘎嘎!


免責聲明!

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



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