2025-02-09

雖然幾乎每種處理器和編程語言都支持浮點運算,但大多數程序員很少註意它。這容易理解 ― 我們中大多數很少需要使用非整數類型。除瞭科學計算和偶爾的計時測試或基準測試程序,其它情況下幾乎都用不著它。同樣,大多數開發人員也容易忽略 java.math.BigDecimal 所提供的任意精度的小數 ― 大多數應用程序不使用它們。然而,在以整數為主的程序中有時確實會出人意料地需要表示非整型數據。例如,JDBC 使用 BigDecimal 作為 SQL DECIMAL 列的首選互換格式。

【IEEE 浮點】
Java 語言支持兩種基本的浮點類型: float 和 double ,以及與它們對應的包裝類 Float 和 Double 。它們都依據 IEEE 754 標準,該標準為 32 位浮點和 64 位雙精度浮點二進制小數定義瞭二進制標準。
IEEE 754 用科學記數法以底數為 2 的小數來表示浮點數。IEEE 浮點數用 1 位表示數字的符號,用 8 位來表示指數,用 23 位來表示尾數,即小數部分。作為有符號整數的指數可以有正負之分。小數部分用二進制(底數 2)小數來表示,這意味著最高位對應著值 ?(2 -1),第二位對應著 ?(2 -2),依此類推。對於雙精度浮點數,用 11 位表示指數,52 位表示尾數。IEEE 浮點值的格式如圖 1 所示。
 


因為用科學記數法可以有多種方式來表示給定數字,所以要規范化浮點數,以便用底數為 2 並且小數點左邊為 1 的小數來表示,按照需要調節指數就可以得到所需的數字。所以,例如,數 1.25 可以表示為尾數為 1.01,指數為 0: (-1) 0*1.01 2*2 0
數 10.0 可以表示為尾數為 1.01,指數為 3: (-1) 0*1.01 2*2 3

【特殊數字】
除瞭編碼所允許的值的標準范圍(對於 float ,從 1.4e-45 到 3.4028235e+38),還有一些表示無窮大、負無窮大、 -0 和 NaN(它代表“不是一個數字”)的特殊值。這些值的存在是為瞭在出現錯誤條件(譬如算術溢出,給負數開平方根,除以 0 等)下,可以用浮點值集合中的數字來表示所產生的結果。
這些特殊的數字有一些不尋常的特征。例如, 0 和 -0 是不同值,但在比較它們是否相等時,被認為是相等的。用一個非零數去除以無窮大的數,結果等於 0 。特殊數字 NaN 是無序的;使用 == 、 < 和 > 運算符將 NaN 與其它浮點值比較時,結果為 false 。如果 f 為 NaN,則即使 (f == f) 也會得到 false 。如果想將浮點值與 NaN 進行比較,則使用 Float.isNaN() 方法。表 1 顯示瞭無窮大和 NaN 的一些屬性。

【表 1. 特殊浮點值的屬性】
表達式 結果
Math.sqrt(-1.0) -> NaN
0.0 / 0.0 -> NaN
1.0 / 0.0 -> 無窮大
-1.0 / 0.0 -> 負無窮大
NaN + 1.0 -> NaN
無窮大 + 1.0 -> 無窮大
無窮大 + 無窮大 -> 無窮大
NaN > 1.0 -> false
NaN == 1.0 -> false
NaN < 1.0 -> false
NaN == NaN -> false
0.0 == -0.01 -> true
基本浮點類型和包裝類浮點有不同的比較行為
使事情更糟的是,在基本 float 類型和包裝類 Float 之間,用於比較 NaN 和 -0 的規則是不同的。對於 float 值,比較兩個 NaN 值是否相等將會得到 false ,而使用 Float.equals() 來比較兩個 NaN Float 對象會得到 true 。造成這種現象的原因是,如果不這樣的話,就不可能將 NaN Float 對象用作 HashMap 中的鍵。類似的,雖然 0 和 -0 在表示為浮點值時,被認為是相等的,但使用 Float.compareTo() 來比較作為 Float 對象的 0 和 -0 時,會顯示 -0 小於 0 。


【浮點中的危險】
由於無窮大、NaN 和 0 的特殊行為,當應用浮點數時,可能看似無害的轉換和優化實際上是不正確的。例如,雖然好象 0.0-f 很明顯等於 -f ,但當 f 為 0 時,這是不正確的。還有其它類似的 gotcha,表 2 顯示瞭其中一些 gotcha。
表 2. 無效的浮點假定
這個表達式…… 不一定等於…… 當……
0.0 – f -f f 為 0
f < g ! (f >= g) f 或 g 為 NaN
f == f true f 為 NaN
f + g – g f g 為無窮大或 NaN
舍入誤差
浮點運算很少是精確的。雖然一些數字(譬如 0.5 )可以精確地表示為二進制(底數 2)小數(因為 0.5 等於 2 -1),但其它一些數字(譬如 0.1 )就不能精確的表示。因此,浮點運算可能導致舍入誤差,產生的結果接近 ― 但不等於 ― 您可能希望的結果。例如,下面這個簡單的計算將得到 2.600000000000001 ,而不是 2.6 :

double s=0;
  for (int i=0; i<26; i++)
    s += 0.1;
  System.out.println(s);


類似的, .1*26 相乘所產生的結果不等於 .1 自身加 26 次所得到的結果。當將浮點數強制轉換成整數時,產生的舍入誤差甚至更嚴重,因為強制轉換成整數類型會舍棄非整數部分,甚至對於那些“看上去似乎”應該得到整數值的計算,也存在此類問題。例如,下面這些語句:

double d = 29.0 * 0.01;
System.out.println(d);
System.out.println((int) (d * 100));


將得到以下輸出:
0.29
  28

這可能不是您起初所期望的。


【浮點數比較指南】
由於存在 NaN 的不尋常比較行為和在幾乎所有浮點計算中都不可避免地會出現舍入誤差,解釋浮點值的比較運算符的結果比較麻煩。
最好完全避免使用浮點數比較。當然,這並不總是可能的,但您應該意識到要限制浮點數比較。如果必須比較浮點數來看它們是否相等,則應該將它們差的絕對值同一些預先選定的小正數進行比較,這樣您所做的就是測試它們是否“足夠接近”。(如果不知道基本的計算范圍,可以使用測試“abs(a/b – 1) < epsilon”,這種方法比簡單地比較兩者之差要更準確)。甚至測試看一個值是比零大還是比零小也存在危險 ―“以為”會生成比零略大值的計算事實上可能由於積累的舍入誤差會生成略微比零小的數字。
NaN 的無序性質使得在比較浮點數時更容易發生錯誤。當比較浮點數時,圍繞無窮大和 NaN 問題,一種避免 gotcha 的經驗法則是顯式地測試值的有效性,而不是試圖排除無效值。在清單 1 中,有兩個可能的用於特性的 setter 的實現,該特性隻能接受非負數值。第一個實現會接受 NaN,第二個不會。第二種形式比較好,因為它顯式地檢測瞭您認為有效的值的范圍。

清單 1. 需要非負浮點值的較好辦法和較差辦法
// Trying to test by exclusion — this doesn't catch NaN or infinity
    public void setFoo(float foo) {
      if (foo < 0)
          throw new IllegalArgumentException(Float.toString(f));
        this.foo = foo;
    }
    // Testing by inclusion — this does catch NaN
    public void setFoo(float foo) {
      if (foo >= 0 && foo < Float.INFINITY)
        this.foo = foo;
  else
        throw new IllegalArgumentException(Float.toString(f));
    }

不要用浮點值表示精確值
一些非整數值(如幾美元和幾美分這樣的小數)需要很精確。浮點數不是精確值,所以使用它們會導致舍入誤差。因此,使用浮點數來試圖表示象貨幣量這樣的精確數量不是一個好的想法。使用浮點數來進行美元和美分計算會得到災難性的後果。浮點數最好用來表示象測量值這類數值,這類值從一開始就不怎麼精確。

 

【用於較小數的 BigDecimal】
從 JDK 1.3 起,Java 開發人員就有瞭另一種數值表示法來表示非整數: BigDecimal 。 BigDecimal 是標準的類,在編譯器中不需要特殊支持,它可以表示任意精度的小數,並對它們進行計算。在內部,可以用任意精度任何范圍的值和一個換算因子來表示 BigDecimal ,換算因子表示左移小數點多少位,從而得到所期望范圍內的值。因此,用 BigDecimal 表示的數的形式為 unscaledValue*10 -scale 。
用於加、減、乘和除的方法給 BigDecimal 值提供瞭算術運算。由於 BigDecimal 對象是不可變的,這些方法中的每一個都會產生新的 BigDecimal 對象。因此,因為創建對象的開銷, BigDecimal 不適合於大量的數學計算,但設計它的目的是用來精確地表示小數。如果您正在尋找一種能精確表示如貨幣量這樣的數值,則 BigDecimal 可以很好地勝任該任務。
所有的 equals 方法都不能真正測試相等
如浮點類型一樣, BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測數值之間是否相等時要小心。 equals() 方法認為,兩個表示同一個數但換算值不同(例如, 100.00 和 100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法會認為這兩個數是相等的,所以在從數值上比較兩個 BigDecimal 值時,應該使用 compareTo() 而不是 equals() 。
另外還有一些情形,任意精度的小數運算仍不能表示精確結果。例如, 1 除以 9 會產生無限循環的小數 .111111… 。出於這個原因,在進行除法運算時, BigDecimal 可以讓您顯式地控制舍入。 movePointLeft() 方法支持 10 的冪次方的精確除法。
使用 BigDecimal 作為互換類型
SQL-92 包括 DECIMAL 數據類型,它是用於表示定點小數的精確數字類型,它可以對小數進行基本的算術運算。一些 SQL 語言喜歡稱此類型為 NUMERIC 類型,其它一些 SQL 語言則引入瞭 MONEY 數據類型,MONEY 數據類型被定義為小數點右側帶有兩位的小數。
如果希望將數字存儲到數據庫中的 DECIMAL 字段,或從 DECIMAL 字段檢索值,則如何確保精確地轉換該數字?您可能不希望使用由 JDBC PreparedStatement 和 ResultSet 類所提供的 setFloat() 和 getFloat() 方法,因為浮點數與小數之間的轉換可能會喪失精確性。相反,請使用 PreparedStatement 和 ResultSet 的 setBigDecimal()及 getBigDecimal() 方法。
對於 BigDecimal ,有幾個可用的構造函數。其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入。要小心使用 BigDecimal(double) 構造函數,因為如果不瞭解它,會在計算過程中產生舍入誤差。請使用基於整數或 String 的構造函數。
構造 BigDecimal 數
對於 BigDecimal ,有幾個可用的構造函數。其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入。要小心使用 BigDecimal(double) 構造函數,因為如果不瞭解它,會在計算過程中產生舍入誤差。請使用基於整數或 String 的構造函數。
如果使用 BigDecimal(double) 構造函數不恰當,在傳遞給 JDBC setBigDecimal() 方法時,會造成似乎很奇怪的 JDBC 驅動程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數字 0.01 存儲到小數字段:
PreparedStatement ps = connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
ps.setString(1, "penny");
ps.setBigDecimal(2, new BigDecimal(0.01));
ps.executeUpdate();

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


【結束語】
在 Java 程序中使用浮點數和小數充滿著陷阱。浮點數和小數不象整數一樣“循規蹈矩”,不能假定浮點計算一定產生整型或精確的結果,雖然它們的確“應該”那樣做。最好將浮點運算保留用作計算本來就不精確的數值,譬如測量。如果需要表示定點數(譬如,幾美元和幾美分),則使用 BigDecimal 。

 

//////////////////////////////////////////////////
以下關於浮點數的內容轉自http://www.balingke.com/archives/737.html
與整型數據不同,浮點數是采用"符號位+指數+有效位(尾數)"的存儲方式,介於這種存儲的特殊性,所以隻占4字節的float類型數據卻可以比占瞭8字節的long類型數據有著更大的取值范圍。然而,這也是要付出一定的代價的,具體情況如下。
(1)浮點數隻是近似的存儲
【浮點數缺陷1】
Java代碼 
public class FloatTest1 { 
    public static void main(String[] args) { 
        int intMax = Integer.MAX_VALUE; 
        float fintMax = intMax; 
         
        int intMin = Integer.MIN_VALUE; 
        float fintMin = intMin; 
         
        long longMax = Long.MAX_VALUE; 
        double dlongMax = longMax; 
         
        long longMin = Long.MIN_VALUE; 
        double dlongMin = longMin; 
         
        System.out.println("int類型的最大值是:" + intMax); 
        System.out.println("使用float存儲後為:" + fintMax); 
        System.out.println("int類型的最小值是:" + intMin); 
        System.out.println("使用float存儲後為:" + fintMin); 
        System.out.println("long類型的最大值是:" + longMax); 
        System.out.println("使用float存儲後為:" + dlongMax); 
        System.out.println("long類型的最小值是:" + longMin); 
        System.out.println("使用float存儲後為:" + dlongMin); 
    } 

 
// 運行結果: 
// int類型的最大值是:2147483647 
// 使用float存儲後為:2.14748365E9 
// int類型的最小值是:-2147483648 
// 使用float存儲後為:-2.14748365E9 
// long類型的最大值是:9223372036854775807 
// 使用float存儲後為:9.223372036854776E18 
// long類型的最小值是:-9223372036854775808 
// 使用float存儲後為:-9.223372036854776E18 

從結果為我們得知,無論是使用float類型的變量來存儲數值,還是用double類型的變量來存儲數值,都損失瞭一定的精度,在使用浮點數來輸出時,都未能輸出準確的結果。

【浮點數的缺陷2】測試一下用浮點數來存儲較小的數據。
Java代碼 
public class FloatTest2 { 
    public static void main(String[] args) { 
        for(float f = 0.1f; f < 1; f += 0.1){ 
            System.out.println(f); 
        } 
        for(double d = 0.1f; d < 1; d += 0.1){ 
            System.out.println(d); 
        } 
    } 

 
// 運行結果: 
// 0.1 
// 0.2 
// 0.3 
// 0.4 
// 0.5 
// 0.6 
// 0.70000005 
// 0.8000001 
// 0.9000001 
// 0.10000000149011612 
// 0.20000000149011612 
// 0.30000000149011613 
// 0.40000000149011616 
// 0.5000000014901161 
// 0.6000000014901161 
// 0.7000000014901161 
// 0.8000000014901161 
// 0.900000001490116 


從結果可知,雖然在計算的時候每次遞增0.1,然而結果卻是如此的不盡人意。從兩個例子可以得出:不論是存儲達的數值,還是小的數值,浮點數都不是十分的準確。浮點數在計算機中隻是近似的存儲,類似於1/3這樣的無限小數,用浮點數也不能準確地表示出來。這裡需要註意的是,在第二個例子中,我們看到0.1~0.6的輸出結果是正確的,而從0.7開始才出現誤差。其實是我們的眼睛受騙瞭。

在執行float f=0.1f時,計算機中的數據並不是純粹的0.1,而是類似於0.1000~000xxx的形式,其中000~000表示若幹個0,xxx表示若幹個數字,假設其數據為0.1000000000111111,而folat類型隻能保留7~8位有效數字,這樣就會從中間截斷,從而float存儲的值成為0.10000000,即浮點數值0.1。

(2)浮點數的大小比較
Java代碼 
public class FloatTest3 { 
    public static void main(String[] args) { 
        double d1 = 0.1; 
        double d2 = 0.2; 
        double d3 = d1 + d2; 
         
        if (d1 + d2 == 0.3) { 
            System.out.println("d1 + d2 == 0.3"); 
        } else { 
            System.out.println("d1 + d2 != 0.3"); 
            System.out.println(d3); 
        } 
    } 

 
// 運行結果: 
// d1 + d2 != 0.3 
// 0.30000000000000004 

再次說明,浮點數的存儲是有一定的誤差的,所以,不要用浮點數進行相等的比較,也不要進行混合比較(如int與float的比較)。

(3)數量級相差很大時浮點數的運算
在使用浮點數時,還需要註意一點,那就是不要再數量級相差太大的數之間進行加減運算,這樣可能無法改變原數量級較大的操作數。情況下例:
Java代碼 
public class FloatTest4 { 
    public static void main(String[] args) { 
        float f1 = 16777216f; 
        float f2 = f1 + 1; 
         
        if(f1 == f2){ 
            System.out.println("f1等於f2"); 
        } else { 
            System.out.println("f1不等於f2"); 
        } 
         
        System.out.println("f1 = " + f1); 
        System.out.println("f2 = " + f2); 
    } 

 
// 運行結果: 
// f1等於f2 
// f1 = 1.6777216E7 
// f2 = 1.6777216E7 

這與浮點數的內部表示有關,這裡可以不必理會浮點數復雜的存儲原理,隻要明白浮點數畢竟隻是近似的存儲,而且其存儲值越大時,損失的精度也越大,對其進行數量級相差較大的加減元素按就會得不到想要的結果。

作者“yuanzhifei89”
 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *