前言
先來看一個double失真的例子
public class DoubleTest {
public static void main(String[] args) {
for (double i = 0; i < 1; i = (i * 10 + 1) / 10) {
for (double k = 0; k < i; k = (k * 10 + 1) / 10) {
System.out.println(i + "-" + k + "=" + (i - k));
}
}
}
}
輸出:
0.1-0.0=0.1
0.2-0.0=0.2
0.2-0.1=0.1
0.3-0.0=0.3
0.3-0.1=0.19999999999999998
0.3-0.2=0.09999999999999998
0.4-0.0=0.4
0.4-0.1=0.30000000000000004
0.4-0.2=0.2
0.4-0.3=0.10000000000000003
0.5-0.0=0.5
0.5-0.1=0.4
0.5-0.2=0.3
0.5-0.3=0.2
0.5-0.4=0.09999999999999998
0.6-0.0=0.6
0.6-0.1=0.5
0.6-0.2=0.39999999999999997
0.6-0.3=0.3
0.6-0.4=0.19999999999999996
0.6-0.5=0.09999999999999998
0.7-0.0=0.7
0.7-0.1=0.6
0.7-0.2=0.49999999999999994
0.7-0.3=0.39999999999999997
0.7-0.4=0.29999999999999993
0.7-0.5=0.19999999999999996
0.7-0.6=0.09999999999999998
0.8-0.0=0.8
0.8-0.1=0.7000000000000001
0.8-0.2=0.6000000000000001
0.8-0.3=0.5
0.8-0.4=0.4
0.8-0.5=0.30000000000000004
0.8-0.6=0.20000000000000007
0.8-0.7=0.10000000000000009
0.9-0.0=0.9
0.9-0.1=0.8
0.9-0.2=0.7
0.9-0.3=0.6000000000000001
0.9-0.4=0.5
0.9-0.5=0.4
0.9-0.6=0.30000000000000004
0.9-0.7=0.20000000000000007
0.9-0.8=0.09999999999999998
一、 什么是浮點數?
1、小數
一個小數的組成:在我國,小數表示由三部分組成,分別是整數+小數點(分隔符)+小數。
2、小數為什么會被稱為浮點數
浮點數是屬于有理數中某特定子集的數的數字表示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這種表示方法類似于基數為10的科學計數法。
對于浮點數可以這樣簡單的理解:浮點數就是小數點可以任意浮動的數字。
在計算機的機器語言中,只有二進制,機器語言只能識別0和1。所以,計算機也是不可能存儲小數的,所以需要有另一種變通的存儲方案。這種方案就是指數方案:
通過觀察以上的圖片不難發現,作為一個小數3.14。如果使用指數表現形式的話(3.14E0),其寫法是多種多樣的,這樣寫的話,小數點就可以任意浮動了。
3、Java中浮點數的表示方法
對于float來說,4個字節,32位,0-22位表示尾數,23-30(8位)表示指數,31位表示符號位。
對于double來說,8個字節,64位,0-51表示尾數,52-62(11位)表示指數,63位最高位表示符號位。
二、浮點數在內存中是如何存儲的?
我們知道,任何數據在計算機內存中都是用‘0\1’來存儲的,浮點數亦是如此。因此十進制浮點數在存儲時必定會轉換為二進制的浮點數。
在內存中使用二進制的科學計數法來存儲,因此分為階碼(即指數)和底數,由于也有正負之分,所以還有一位符號位。
以float為例,float在內存中的存儲為:
float 符號位(1bit) 指數(8 bit) 尾數(23 bit)
double 符號位(1bit) 指數(11 bit) 尾數(52 bit)
float在內存中占8位,由于階碼實際存儲的是指數的移碼,假設指數的真值是e,階碼為E,則有E=e+(2^n-1 -1)。其中 2^n-1 -1是IEEE754標準規定的指數偏移量,根據這個公式我們可以得到 2^8 -1=127。于是,float的指數范圍為-128 +127,而double的指數范圍為-1024 +1023。其中負指數決定了浮點數所能表達的絕對值最小的非零數;而正指數決定了浮點數所能表達的絕對值最大的數,也即決定了浮點數的取值范圍。
float的范圍為-2^128 ~ +2^127,也即-3.40E+38 ~ +3.40E+38;
double的范圍為-2^1024 ~ +2^1023,也即-1.79E+308 ~ +1.79E+308
這里使用移位存儲,對于float來說,指數位加上127,double位加上1023(這里指的是存儲,在比較的時候要分別減去127和1023)
移位存儲本質上是為了保證+0和-0的一致性。
以float指數部分的這8位來分析,
那么這8位組成的新的字節,我們來用下面的一串數字表示:0000 0000
首先,我們假設不使用移位存儲技術,而是單單看看這個 8位組成的新字節,到底能表示多少個數: 0000 0000 -1111 1111 即0-255,一共256個數。
但是我們知道這8位數既要表示正數也要表示負數。
所以將左邊第一位拿出來表示正負的符號:
第一個區間:
0 000 0000 - 0 111 1111
即+0 到127
第二個區間:
1 000 0000 - 1 111 1111
即 -0到-127
這就是問題的所在:怎么會有兩個0,一個正零,一個負零。
這時候使用移位存儲:float使用127(0111 1111)
表示0:0+127=127 即 0000 0000 +0111 1111=0111 1111
表示1:1+127=128 即 0000 0001 +0111 1111=1000 0000
表示128:128+127=255 即 1000 0000+0111 1111=1111 1111
最大的正數,再大就要溢出了。
表示-1: -1+127=126=127-1 即 0111 1111-0000 0001=0111 1110
表示-1: -2+127=125=127-2 即 0111 1111-0000 0010=0111 1101
表示-127: -127+127=0 即0111 1111-0111 1111=0000 0000
最小的負數,在校就溢出了。
三、浮點數的進制轉換
1、十進制轉二進制
主要看看十進制轉二進制,整數部分和小數部分分開處理
整數部分:整數除以2,得到一個商和余數,得到的商繼續除以2并得到一個商和一個余數,繼續除以2操作直至商為0,上述操作得到一系列余數,從最后一個余數開始直至第一個余數,這一系列0\1即為轉換后的二進制數。
小數部分:乘以2,然后取出整數部分,將剩下的小數部分繼續乘以2,然后再取整數部分,一直取到小數部分為零為止。如果永遠不為零,則按要求保留足夠位數的小數,最后一位做0舍1入。將取出的整數順序排列。
從以上轉換過程可以看出,并不是任何一個十進制小數都可以用二進制精確表示出來。一個在0到1之間的小數P可用如下形式表示:
從這個式子中我們也可看出二進制表示出的小數是分段的,這也是為什么在Java中浮點數很多時候并不是十分精確的表示十進制小數的根本原因。
public static void main(String[] args) {
float f1=20f;
float f2=20.3f;
float f3=20.5f;
double d1=20;
double d2=20.3;
double d3=20.5;
System.out.println(f1==d1);
System.out.println(f2==d2);
System.out.println(f3==d3);
}
true
false
true
以20.3舉例:
20轉換后變為 10100
0.3 要轉換二進制,需要乘2, 乘完之后 取整數部分,然后用乘的結果減去整數部分, 然后 接著乘2, 直至最后沒有小數或者小數出現循環, 即乘完.
0.3 * 2 = 0.6 (0)
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,進入循環了,所以,結果
0.3 = 0.010011001…1001
所以20.3 = 10100.010011001…1001 (二進制).
2、二進制的科學記數法表示
20.3 = 10100.010011001…1001 (二進制)=1.01000100110011E10…..(十進制科學計數)=1.01000100110011E100…..(二進制科學計數)
這里使用移位存儲,對于float來說,指數位加上127,double位加上1023(這里指的是存儲,在比較的時候要分別減去127和1023)
同時要注意一點,以float為例,最高位表示的是整個數的符號位,指數位一共8位,最高位表示的是指數位的正負,因為有可能是E-100這樣的情況,所以雖然有8位,最高位只是符號位,剩下7位才是表示真正的數值,這也是使用移位存儲的原因。
對于一個數字,只要不超過和float的范圍,同時小數部分不是無限小數,就可以和對應的double類型相等。
3、浮點數舍入規則
以52位尾數的雙精度浮點數為例,舍入時需要重點參考第53位。
若第53位為1,而其后的位數都是0,此時就要使第52位為0;若第52位為0則不用再進行其他操作,若第52位為1,則第53位就要向52位進一位。
若第53位為1,但其后的位數不全為0,則第53為就要向第52位進一位。
若不是以上兩種情況,也即53位為0,那么就直接舍棄不進位,稱為下舍入。
浮點數舍入規則也就證明了為何在上文中提到的浮點數舍入中,相對舍入誤差不能大于機器ε的一半。
對于java來說,一般float類型小數點后保留7位,而double類型小數點后保留15位。
這個原因也是因為尾數的數據寬度限制
對于float型來說,因為2^23 = 8388608
同時最左一位默認省略了,故實際能表示2^24 = 16777216個數,最多能表示8位,但絕對精確的只能表示7位。
而對于double型來說,2^52 = 4503599627370496,共16位。加上省略的一位,能表示2^53 = 9007199254740992。故double型最多能表示16位,而絕對精確的只能表示15位。
4、機器ε
機器ε表示1與大于1的最小浮點數之差。不同精度定義的機器ε不同。以雙精度為例,
雙精度表示1是
1.000......0000(52個0) × 2^0
而比1大的最小的雙精度是(其實還能表示更小的范圍,后文中會提到,但并不影響這里的機器ε)
1.000......0001 × 2^0
也即
2^-52 ≈ 2.220446049250313e-16。所以它就是雙精度浮點數的機器ε。
在舍入中,相對舍入誤差不能大于機器ε的一半。
對于雙精度浮點數來說,這個值為0.00000005960464477539。
所以在Java中double類型中連續8個0.1相乘,就會出現表示不精確的情況。