Double為什么會失真?

前言

先來看一個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、小數

一個小數的組成:在我國,小數表示由三部分組成,分別是整數+小數點(分隔符)+小數。


圖片.png

2、小數為什么會被稱為浮點數

浮點數是屬于有理數中某特定子集的數的數字表示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這種表示方法類似于基數為10的科學計數法。

對于浮點數可以這樣簡單的理解:浮點數就是小數點可以任意浮動的數字。

在計算機的機器語言中,只有二進制,機器語言只能識別0和1。所以,計算機也是不可能存儲小數的,所以需要有另一種變通的存儲方案。這種方案就是指數方案:


圖片.png

通過觀察以上的圖片不難發現,作為一個小數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在內存中的存儲為:


圖片.png

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可用如下形式表示:


圖片.png

從這個式子中我們也可看出二進制表示出的小數是分段的,這也是為什么在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相乘,就會出現表示不精確的情況。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容