float丟失精度的解決

java在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數進行精確的運算。雙精度浮點型變量double可以處理16位有效數。在實際應用中,需要對更大或者更小的數進行運算和處理。float和double只能用來做科學計算或者是工程計算,在商業計算中要用java.math.BigDecimal。BigDecimal所創建的是對象,我們不能使用傳統的+、-、*、/等算術運算符直接對其對象進行數學運算,而必須調用其相對應的方法。

System.out.println(new BigDecimal(Double.toString(0.05)).add(new BigDecimal(Double.toString(0.01))));
System.out.println(new BigDecimal(Double.toString(1.0)).subtract(new BigDecimal(Double.toString(0.42))));
System.out.println(new BigDecimal(Double.toString(4.015)).multiply(new BigDecimal(Integer.toString(100))));
System.out.println(new BigDecimal(Double.toString(123.3)).divide(new BigDecimal(Integer.toString(100))));


以下內容轉載:

package test1;

public class Test2 {

/**
* @param args
*/
public static void main(String[] args) {
  Float xx = 2.0f;
  Float yy = 1.8f;
  Float tt = xx - yy;
  System.out.println("tttttt-----" + tt);

}

}

果然輸出結果是: tttttt-----0.20000005

再測試了幾個float類型的減法,除了*.0這樣的相減沒有異議之外,都存在這個問題,就是說float在相減的時候精度丟失了。後來在網上找到一段解決這個問題的辦法,記在這裏:

package test1;

import java.math.BigDecimal;

public class Test2 {

/**
* @param args
*/
public static void main(String[] args) {
  Float xx = 2.2f;
  Float yy = 2.0f;
  Float tt = xx - yy;

BigDecimal b1 = new BigDecimal(Float.toString(xx));
  BigDecimal b2 = new BigDecimal(Float.toString(yy));
  float ss = b1.subtract(b2).floatValue();
  System.out.println("ssss----" + ss);
  System.out.println("tttttt-----" + tt);
}
}
輸出爲:

ssss----0.2
tttttt-----0.20000005

這樣一對比,差異就很明顯了。

解決了問題,再找了一下爲什麼會產生這種差異:

網上有篇文章寫得很詳細,標題爲《剖析float型的內存存儲和精度丟失問題》,全文內容如下:

問題提出:12.0f-11.9f=0.10000038,"減不盡"爲什麼?

現在我們就詳細剖析一下浮點型運算爲什麼會造成精度丟失?

1、小數的二進制表示問題

首先我們要搞清楚下面兩個問題:

    (1) 十進制整數如何轉化爲二進制數

          算法很簡單。舉個例子,11表示成二進制數:

                    11/2=5 餘   1

                      5/2=2   餘   1

                      2/2=1   餘   0

                      1/2=0   餘   1

                         0結束         11二進制表示爲(從下往上):1011

         這裏提一點:只要遇到除以後的結果爲0了就結束了,大家想一想,所有的整數除以2是不是一定能夠最終得到0。換句話說,所有的整數轉變爲二進制數的算法會不會無限循環下去呢?絕對不會,整數永遠可以用二進制精確表示,但小數就不一定了。

     (2) 十進制小數如何轉化爲二進制數

          算法是乘以2直到沒有了小數爲止。舉個例子,0.9表示成二進制數

                    0.9*2=1.8   取整數部分 1

                    0.8(1.8的小數部分)*2=1.6    取整數部分 1

                    0.6*2=1.2   取整數部分 1

                    0.2*2=0.4   取整數部分 0

                    0.4*2=0.8   取整數部分 0

                    0.8*2=1.6 取整數部分 1

                    0.6*2=1.2   取整數部分 0

                             .........      0.9二進制表示爲(從上往下): 1100100100100......

          注意:上面的計算過程循環了,也就是說*2永遠不可能消滅小數部分,這樣算法將無限下去。很顯然,小數的二進制表示有時是不可能精確的。其實道理很簡單,十進制系統中能不能準確表示出1/3呢?同樣二進制系統也無法準確表示1/10。這也就解釋了爲什麼浮點型減法出現了"減不盡"的精度丟失問題。

2、float型在內存中的存儲

    衆所周知、 Java 的float型在內存中佔4個字節。float的32個二進制位結構如下

float內存存儲結構

            4bytes      31    30    29----23    22----0        

表示       實數符號位    指數符號位        指數位          有效數位

       其中符號位1表示正,0表示負。有效位數位24位,其中一位是實數符號位。

將一個float型轉化爲內存存儲格式的步驟爲:

(1)先將這個實數的絕對值化爲二進制格式,注意實數的整數部分和小數部分的二進制方法在上面已經探討過了。
    (2)將這個二進制格式實數的小數點左移或右移n位,直到小數點移動到第一個有效數字的右邊。
    (3)從小數點右邊第一位開始數出二十三位數字放入第22到第0位。
    (4)如果實數是正的,則在第31位放入“0”,否則放入“1”。
    (5)如果n 是左移得到的,說明指數是正的,第30位放入“1”。如果n是右移得到的或n=0,則第30位放入“0”。
    (6)如果n是左移得到的,則將n減去1後化爲二進制,並在左邊加“0”補足七位,放入第29到第23位。如果n是右移得到的或n=0,則將n化爲二進制後在左邊加“0”補足七位,再各位求反,再放入第29到第23位。

舉例說明: 11.9的內存存儲格式

      (1) 將11.9化爲二進制後大約是"1011.1110011001100110011001100..."。

      (2) 將小數點左移三位到第一個有效位右側: "1. 01111100110011001100110"。保證有效位數24位,右側多餘的截取(誤差在這裏產生了)。

      (3) 這已經有了二十四位有效數字,將最左邊一位“1”去掉,得到“01111100110011001100110”共23bit。將它放入float存儲結構的第22到第0位。

      (4) 因爲11.9是正數,因此在第31位實數符號位放入“0”。

      (5) 由於我們把小數點左移,因此在第30位指數符號位放入“1”。

      (6) 因爲我們是把小數點左移3位,因此將3減去1得2,化爲二進制,並補足7位得到0000010,放入第29到第23位。

          最後表示11.9爲: 0 10000010 01111100110011001100110

再舉一個例子:0.2356的內存存儲格式
     (1)將0.2356化爲二進制後大約是0.00111100010100000100100000。
     (2)將小數點右移三位得到1.11100010100000100100000。
     (3)從小數點右邊數出二十三位有效數字,即11100010100000100100000放
入第22到第0位。
     (4)由於0.2356是正的,所以在第31位放入“0”。
     (5)由於我們把小數點右移了,所以在第30位放入“0”。
     (6)因爲小數點被右移了3位,所以將3化爲二進制,在左邊補“0”補足七
位,得到0000011,各位取反,得到1111100,放入第29到第23位。

最後表示0.2356爲:001111100 11100010100000100100000

將一個內存存儲的float二進制格式轉化爲十進制的步驟:
    (1)將第22位到第0位的二進制數寫出來,在最左邊補一位“1”,得到二十四位有效數字。將小數點點在最左邊那個“1”的右邊。
    (2)取出第29到第23位所表示的值n。當30位是“0”時將n各位求反。當30位是“1”時將n增1。
    (3)將小數點左移n位(當30位是“0”時)或右移n位(當30位是“1”時),得到一個二進制表示的實數。
    (4)將這個二進制實數化爲十進制,並根據第31位是“0”還是“1”加上正號或負號即可。

3、浮點型的減法運算

浮點加減運算過程比定點運算過程複雜。完成浮點加減運算的操作過程大體分爲四步:
(1) 0操作數的檢查;

如果判斷兩個需要加減的浮點數有一個爲0,即可得知運算結果而沒有必要再進行有序的一些列操作。

(2) 比較階碼(指數位)大小並完成對階;

兩浮點數進行加減,首先要看兩數的 指數位 是否相同,即小數點位置是否對齊。若兩數 指數位 相同,表示小數點是對齊的,就可以進行尾數的加減運算。反之,若兩數階碼不同,表示小數點位置沒有對齊,此時必須使兩數的階碼相同,這個過程叫做對階

如何對階(假設兩浮點數的指數位爲 Ex 和 Ey):

通過尾數的移位以改變 Ex 或 Ey ,使之相等。 由 於浮點表示的數多是規格化的,尾數左移會引起最高有位的丟失,造成很大誤差;而尾數右移雖引起最低有效位的丟失,但造成的誤差較小,因此,對階操作規定使 尾數右移,尾數右移後使階碼作相應增加,其數值保持不變。很顯然,一個增加後的階碼與另一個相等,所增加的階碼一定是小階。因此在對階時,總是使小階向大階看齊,即小階的尾數向右移位 ( 相當於小數點左移 ) ,每右移一位,其階碼加 1 ,直到兩數的階碼相等爲止,右移的位數等於階差 △ E
(3) 尾數(有效數位)進行加或減運算;

              對階完畢後就可 有效數位 求和。 不論是加法運算還是減法運算,都按加法進行操作,其方法與定點加減運算完全一樣。
(4) 結果規格化並進行舍入處理。

4、 計算12.0f-11.9f

12.0f 的內存存儲格式爲: 0 1 0000010 10000000000000000000000    

    11.9f 的內存存儲格式爲:   0 1 0000010 01111100110011001100110

    可見兩數的指數位完全相同,只要對有效數位進行減法即可。

    12.0f-11.9f   結果:         0 1 0000010 00000011001100110011010

    將結果還原爲十進制爲: 0.00011001100110011010=0.10000038


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章