問題提出:12.0f-11.9f=0.10000038,"減不盡"爲何?java
來自MSDN的解釋:程序員
http://msdn.microsoft.com/zh-cn/c151dt3s.aspx算法
爲什麼浮點數可能丟失精度浮點十進制值一般沒有徹底相同的二進制表示形式。 這是 CPU 所採用的浮點數據表示形式的反作用。 爲此,可能會經歷一些精度丟失,而且一些浮點運算可能會產生意外的結果。數據結構
致使此行爲的緣由是下面之一:學習
十進制數的二進制表示形式可能不精確。編碼
使用的數字之間類型不匹配(例如,混合使用浮點型和雙精度型)。.net
爲解決此行爲,大多數程序員或是確保值比須要的大或者小,或是獲取並使用能夠維護精度的二進制編碼的十進制 (BCD) 庫。blog
如今咱們就詳細剖析一下浮點型運算爲何會形成精度丟失?ip
一、小數的二進制表示問題內存
首先咱們要搞清楚下面兩個問題:
(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。這也就解釋了爲何浮點型減法出現了"減不盡"的精度丟失問題。
二、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爲: 01000001001111100110011001100110
再舉一個例子: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」加上正號或負號便可。
三、浮點型的減法運算
浮點加減運算過程比定點運算過程複雜。完成浮點加減運算的操做過程大致分爲四步:
(1) 0操做數的檢查;
若是判斷兩個須要加減的浮點數有一個爲0,便可得知運算結果而沒有必要再進行有序的一些列操做。
(2) 比較階碼(指數位)大小並完成對階;
兩浮點數進行加減,首先要看兩數的指數位是否相同,即小數點位置是否對齊。若兩數指數位相同,表示小數點是對齊的,就能夠進行尾數的加減運算。反之,若兩數階碼不一樣,表示小數點位置沒有對齊,此時必須使兩數的階碼相同,這個過程叫作對階。
如何對階(假設兩浮點數的指數位爲Ex和Ey):
經過尾數的移位以改變Ex或Ey,使之相等。因爲浮點表示的數可能是規格化的,尾數左移會引發最高有位的丟失,形成很大偏差;而尾數右移雖引發最低有效位的丟失,但形成的偏差較小,所以,對階操做規定使尾數右移,尾數右移後使階碼做相應增長,其數值保持不變。很顯然,一個增長後的階碼與另外一個相等,所增長的階碼必定是小階。所以在對階時,老是使小階向大階看齊,即小階的尾數向右移位(至關於小數點左移),每右移一位,其階碼加1,直到兩數的階碼相等爲止,右移的位數等於階差△E。
(3) 尾數(有效數位)進行加或減運算;
對階完畢後就可有效數位求和。不管是加法運算仍是減法運算,都按加法進行操做,其方法與定點加減運算徹底同樣。
(4) 結果規格化並進行舍入處理。
略
四、計算12.0f-11.9f
12.0f 的內存存儲格式爲: 01000001010000000000000000000000
11.9f 的內存存儲格式爲: 01000001001111100110011001100110
可見兩數的指數位徹底相同,只要對有效數位進行減法便可。
12.0f-11.9f 結果: 01000001000000011001100110011010
將結果還原爲十進制爲: 0.00011001100110011010=0.10000038
詳細的分析
因爲對float或double 的使用不當,可能會出現精度丟失的問題。問題大概狀況能夠經過以下代碼理解:
view plaincopy to clipboardprint?
public class FloatDoubleTest {
public static void main(String[] args) {
float f = 20014999;
double d = f;
double d2 = 20014999;
System.out.println("f=" + f);
System.out.println("d=" + d);
System.out.println("d2=" + d2);
}
}
public class FloatDoubleTest {
public static void main(String[] args) {
float f = 20014999;
double d = f;
double d2 = 20014999;
System.out.println("f=" + f);
System.out.println("d=" + d);
System.out.println("d2=" + d2);
}
}
獲得的結果以下:
f=2.0015E7
d=2.0015E7
d2=2.0014999E7
從輸出結果能夠看出double 能夠正確的表示20014999 ,而float 沒有辦法表示20014999 ,獲得的只是一個近似值。這樣的結果很讓人訝異。20014999 這麼小的數字在float下沒辦法表示。因而帶着這個問題,作了一次關於float和double學習,作個簡單分享,但願有助於你們對java 浮點數的理解。
關於 java 的 float 和 double
Java 語言支持兩種基本的浮點類型: float 和 double 。java 的浮點類型都依據 IEEE 754 標準。IEEE 754 定義了32 位和 64 位雙精度兩種浮點二進制小數標準。
IEEE 754 用科學記數法以底數爲 2 的小數來表示浮點數。32 位浮點數用 1 位表示數字的符號,用 8 位來表示指數,用 23 位來表示尾數,即小數部分。做爲有符號整數的指數能夠有正負之分。小數部分用二進制(底數 2 )小數來表示。對於64 位雙精度浮點數,用 1 位表示數字的符號,用 11 位表示指數,52 位表示尾數。以下兩個圖來表示:
float(32位):
double(64位):
都是分爲三個部分:
(1) 一個單獨的符號位s 直接編碼符號s 。
(2)k 位的冪指數E ,移碼錶示 。
(3)n 位的小數,原碼錶示 。
那麼 20014999 爲何用 float 沒有辦法正確表示?
結合float和double的表示方法,經過分析 20014999 的二進制表示就能夠知道答案了。
如下程序能夠得出 20014999 在 double 和 float 下的二進制表示方式。
view plaincopy to clipboardprint?
public class FloatDoubleTest3 {
public static void main(String[] args) {
double d = 8;
long l = Double.doubleToLongBits(d);
System.out.println(Long.toBinaryString(l));
float f = 8;
int i = Float.floatToIntBits(f);
System.out.println(Integer.toBinaryString(i));
}
}
public class FloatDoubleTest3 {
public static void main(String[] args) {
double d = 8;
long l = Double.doubleToLongBits(d);
System.out.println(Long.toBinaryString(l));
float f = 8;
int i = Float.floatToIntBits(f);
System.out.println(Integer.toBinaryString(i));
}
}
輸出結果以下:
Double:100000101110011000101100111100101110000000000000000000000000000
Float:1001011100110001011001111001100
對於輸出結果分析以下。對於都不 double 的二進制左邊補上符號位 0 恰好能夠獲得 64 位的二進制數。根據double的表示法,分爲符號數、冪指數和尾數三個部分以下:
0 10000010111 0011000101100111100101110000000000000000000000000000
對於 float 左邊補上符號位 0 恰好能夠獲得 32 位的二進制數。 根據float的表示法, 也分爲 符號數、冪指數和尾數三個部分以下 :
0 10010111 00110001011001111001100
綠色部分是符號位,紅色部分是冪指數,藍色部分是尾數。
對比能夠得出:符號位都是 0 ,冪指數爲移碼錶示,二者恰好也相等。惟一不一樣的是尾數。
在 double 的尾數爲: 001100010110011110010111 0000000000000000000000000000 ,省略後面的零,至少須要24位才能正確表示 。
而在 float 下面尾數爲: 00110001011001111001100 ,共 23 位。
爲何會這樣?緣由很明顯,由於 float尾數 最多隻能表示 23 位,因此 24 位的 001100010110011110010111 在 float 下面通過四捨五入變成了 23 位的 00110001011001111001100 。因此 20014999 在 float 下面變成了 20015000 。也就是說 20014999 雖然是在float的表示範圍以內,但 在 IEEE 754 的 float 表示法精度長度沒有辦法表示出 20014999 ,而只能經過四捨五入獲得一個近似值。