[ JAVA編程 ] double類型計算精度丟失問題及解決方法


前言 

  如果你在測試金融相關產品,請務必覆蓋交易金額為小數的場景。特別是使用Java語言的初級開發。

Java基本實例

先來看Java中double類型數值加、減、乘、除計算式實例:

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

運行結果如下:

D:\linyfeng>java Test
0.06+0.01 = 0.06999999999999999
1.0-0.42 = 0.5800000000000001
4.015*100 = 401.49999999999994
303.1/1000 = 0.30310000000000004

    我們發現,計算出來的值和我們預期結果不一致。原因在於我們的計算機是二進制的。浮點數沒有辦法使用二進制進行精確表示。計算機的CPU表示浮點數由兩個部分組成:指數和尾數,這樣的表示方法一般都會失去一定的精確度,有些浮點數運算也會產生一定的誤差。如:2.4的二進制表示並非就是精確的2.4。反而最為接近的二進制表示是 2.3999999999999999。浮點數的值實際上是由一個特定的數學公式計算得到的。可參考http://blog.csdn.net/abing37/article/details/5332798

    那么我們如何才能夠獲取我們想要的預期結果呢?特別是在處理金額交易計算上。其實java的float只能用來進行科學計算或工程計算,在大多數的商業計算中,一般采用java.math.BigDecimal類來進行精確計算。在使用BigDecimal類來進行計算的時候,主要分為以下步驟:

  (1) 用float或者double變量構建BigDecimal對象。通常使用BigDecimal的構造方法或者靜態方法的valueOf()方法把基本類型的變量構建成BigDecimal對象。

  (2) 通過調用BigDecimal的加,減,乘,除等相應的方法進行算術運算。

  (3) 把BigDecimal對象轉換成float,double,int等類型。

BigDecimal類基本介紹

    在修改實例之前,我們先簡單了解一下BigDecimal類的構造函數和成員方法。

BigDecimal(int var)  //創建一個具有參數所指定整數值的對象。
BigDecimal(double var) //創建一個具有參數所指定雙精度值的對象。
BigDecimal(long var)  //創建一個具有參數所指定長整數值的對象。
BigDecimal(String var) //創建一個具有參數所指定以字符串表示的數值的對象。

  成員方法(BigDecimal 的運算方式 不支持 + - * / 這類的運算 它有自己的運算方法)

BigDecimal add(BigDecimal augend)  //加法運算
BigDecimal subtract(BigDecimal subtrahend) //減法運算
BigDecimal multiply(BigDecimal multiplicand) //乘法運算
BigDecimal divide(BigDecimal divisor) //除法運算

  好,既然我們知道方法了,那么我們就用新方法來解決一下上述的問題。修改一下代碼,如下:

import java.math.*;

public class Test{
    public static void main(String [] args){
        double d1 = 0.06;
        double d2 = 0.01;
        BigDecimal b1 = new BigDecimal(d1);
        BigDecimal b2 = new BigDecimal(d2);
        
        System.out.println(b1.add(b2).doubleValue());
        
        double d3 = 1.0;
        double d4 = 0.42;
        BigDecimal b3 = new BigDecimal(d3);
        BigDecimal b4 = new BigDecimal(d4);
        System.out.println(b3.subtract(b4).doubleValue());
        
        double d5 = 4.015;
        double d6 = 100;
        BigDecimal b5 = new BigDecimal(d5);
        BigDecimal b6 = new BigDecimal(d6);
        System.out.println(b5.multiply(b6).doubleValue());
        
        double d7 = 303.1;
        double d8 = 1000;
        BigDecimal b7 = new BigDecimal(d7);
        BigDecimal b8 = new BigDecimal(d8);
        System.out.println(b7.divide(b8).doubleValue());    
    }
}

運行結果:

D:\linyfeng>java Test
0.06999999999999999
0.5800000000000001
401.49999999999994
0.30310000000000004

  我們發現結果還是不對。從上述實例我們知道調用的構造方法為BigDecimal(double var)。 BigDecimal(double val)將 double 轉換為 BigDecimal,后者是double的二進制浮點值准確的十進制表示形式。返回的BigDecimal的標度是使 (10scale × val) 為整數的最小值。這里也幾個特別要注意的地方:

  (1)此構造方法的結果有一定的不可預知性。有人可能認為在 Java 中寫入 new BigDecimal(0.1) 所創建的 BigDecimal 正好等於 0.1(非標度值 1,其標度為 1),但是它實際上等於 0.1000000000000000055511151231257827021181583404541015625。這是因為 0.1 無法准確地表示為 double(或者說對於該情況,不能表示為任何有限長度的二進制小數)。這樣,傳入 到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。
  (2)另一方面,String 構造方法是完全可預知的:寫入 new BigDecimal("0.1") 將創建一個 BigDecimal,它正好 等於預期的 0.1。因此,比較而言,通常建議優先使用 String 構造方法。
  (3)當 double 必須用作 BigDecimal 的源時,請注意,此構造方法提供了一個准確轉換;它不提供與以下操作相同的結果:先使用 Double.toString(double) 方法,然后使用 BigDecimal(String) 構造方法,將 double 轉換為 String。要獲取該結果,請使用 static valueOf(double) 方法。

  根據上述描述,我們繼續修改下例子,修改后如下:

import java.math.*;

public class Test{
    public static void main(String [] args){
        double d1 = 0.06;
        double d2 = 0.01;
        BigDecimal b1 = new BigDecimal(Double.toString(d1));
        BigDecimal b2 = new BigDecimal(Double.toString(d2));
        
        System.out.println(b1.add(b2).doubleValue());
        
        double d3 = 1.0;
        double d4 = 0.42;
        BigDecimal b3 = new BigDecimal(Double.toString(d3));
        BigDecimal b4 = new BigDecimal(Double.toString(d4));
        System.out.println(b3.subtract(b4).doubleValue());
        
        double d5 = 4.015;
        double d6 = 100;
        BigDecimal b5 = new BigDecimal(Double.toString(d5));
        BigDecimal b6 = new BigDecimal(Double.toString(d6));
        System.out.println(b5.multiply(b6).doubleValue());
        
        double d7 = 303.1;
        double d8 = 1000;
        BigDecimal b7 = new BigDecimal(Double.toString(d7));
        BigDecimal b8 = new BigDecimal(Double.toString(d8));
        System.out.println(b7.divide(b8).doubleValue());    
    }
}

運行結果如下:

D:\linyfeng>java Test
0.07
0.58
401.5
0.3031

  計算精度正確。

總結

(1)需要精確的表示兩位小數時我們需要把他們轉換為BigDecimal對象,然后再進行運算。
(2)使用BigDecimal(double val)構造函數時仍會存在精度丟失問題,建議使用BigDecimal(String val)。這就需要先把double類型(調用Double.toString(double var))轉換為字符串然后在作為BigDecimal(String val)構造函數的參數。轉換為BigDecimal對象之后再進行加減乘除操作,這樣精度就不會出現問題了。這也是為什么有關金錢數據存儲都使用BigDecimal。

參考文檔

1、使用BigDecimal進行精確運算

2、java中double和float精度丟失問題及解決方法

3、百度百科BigDecimal


免責聲明!

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



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