【計算機基礎】在0和1的世界裏來來回回

事物的正反兩面被哲學家討論了幾千年。計算機裏的0和1也照舊玩出了各類花樣。java

二進制數 VS 十進制數

本小節講二進制寫法,以及到十進制的轉換方法,若是已熟悉這些內容能夠直接跳到下一小節。git

咱們生活在一個十進制的世界中。10個一毛就是一塊,10個一兩就是一斤。在數學上有滿十進一或借一當十。github

十進制數的基數就是0到9,所以全部的十進制數都是由0到9這九個數字組合出來的。數組

計算機底層處理的都是二進制數,能夠對比十進制數來看看二進制數的特色:ide

滿二進一或借一當二,基數是0和1,就是說全部的二進制數都是由0和1這兩個數字組合出來的。工具

就十進制而言,十個1已經達到「滿十」的條件,因此要「進一」,因而就是10,這是個十進制數,它的值就是十,由於是十個1合在了一塊兒。測試

就二進制而言,兩個1已經達到「滿二」的條件,因此要「進一」,因而就是10,這是個二進制數,它的值就是二,由於是兩個1合在了一塊兒。設計

若是剛剛這個明白了,結合十進制和二進制的特色,接下來就很是容易理解了:code

1 + 1 = 2 -> 10。get

1 + 1 + 1 = 3 = 2 + 1 -> 10 + 1 -> 11。

1 + 1 + 1 + 1 = 4 = 3 + 1 -> 11 + 1 -> 100。

照此類推,列出幾個十進制和對應的二進制:

0 -> 000

1 -> 001

2 -> 010

3 -> 011

4 -> 100

5 -> 101

接下來嘗試找出二進制和十進制之間的換算關係。

首先,十進制數是怎麼使用每一個位置上的數字表示出來的呢?相信全部人都很熟悉。以下面示例:

123 -> 100 + 20 + 3

123 -> 1 100 + 2 10 + 3 * 1

因十進制滿十進一,要想辦法和十聯繫起來,100就是10的2次方,10是10的1次方,1是10的0次方,因而:

123 -> 1 10 ^ 2 + 2 10 ^ 1 + 3 * 10 ^ 0;

進而,咱們發現百位的位置是3,但次方倒是2,正好是3減去1,十位的位置是2,但次方是1,正好是2減去1,個位就是1減去1,也就是0次方了。

因而,這個公式就出來了,太簡單了,你們都知道,就不寫了。

而後,咱們把這個「套路」搬到二進制數裏試試看吧,只不過二進制數是滿二進一,所以要用2的次方。

000 -> 0 2 ^ 2 + 0 2 ^ 1 + 0 2 ^ 0
000 -> 0
4 + 0 2 + 0 1 -> 0
000 -> 0

001 -> 0 2 ^ 2 + 0 2 ^ 1 + 1 2 ^ 0
001 -> 0
4 + 0 2 + 1 1 -> 1
001 -> 1

010 -> 0 2 ^ 2 + 1 2 ^ 1 + 0 2 ^ 0
010 -> 0
4 + 1 2 + 0 1 -> 2
010 -> 2

011 -> 0 2 ^ 2 + 1 2 ^ 1 + 1 2 ^ 0
011 -> 0
4 + 1 2 + 1 1 -> 3
011 -> 3

100 -> 1 2 ^ 2 + 0 2 ^ 1 + 0 2 ^ 0
100 -> 1
4 + 0 2 + 0 1 -> 4
100 -> 4

101 -> 1 2 ^ 2 + 0 2 ^ 1 + 1 2 ^ 0
101 -> 1
4 + 0 2 + 1 1 -> 5
101 -> 5

咱們發現算出來的正好都是其對應的十進制數。這是巧合嗎?固然不是了。其實:

這就是二進制數向十進制數的轉化方法。

咱們也能夠模仿數學,推導出個公式來:

d = b(n) + b(n - 1) + ... + b(1) + b(0)

b(n) = a * 2 ^ n,(a = {0、1},n >= 0)

就是把二進制數的每一位轉化爲十進制數,再加起來便可。

負數的二進制 VS 正數的二進制

上一小節都是以正數舉例。除了正數以外,還有負數和零。

所以,計算機界規定,在須要考慮正負的時候,二進制的最高位就是符號位。

即這個位置上的0或1是用來表示數值符號的,而非用來計算數值的,且規定:

0表示爲正數,1表示爲負數。

那0既不是正數也不是負數,該怎麼表示呢?把0的二進制輸出一下:

0 -> 00000000

發現全是0,最高位也是0,所以0是一種特殊狀況。

接下來開始講解負數的二進制表示,保證看完後有一種「恍然大悟」的感受(若是沒有,那我也沒辦法),哈哈。

長期以來受數學的影響,要把一個正數變成對應的負數,只需在前面加一個負號「-」便可。

基於此,再結合上面計算機界的規定,咱們很容易想固然的認爲,一個正數只要把它的最高位由0設置爲1就變成了對應的負數,像這樣:

由於 1的二進制是,00000001

因此-1的二進制是,10000001

鄭重聲明,這是錯誤的。繼續往下看就知道緣由了。

首先會從官方的角度給出正確結果(裝b用的),而後會從我的的角度給出正確結果(恍然大悟用的)。

站在官方(或學術)的角度,先引入三個概念:

原碼:把一個數看成正數(負數的話把負號去掉便可),它的二進制表示就叫原碼。

反碼:把原碼中的0變成一、1變成0(即0和1對調),所獲得的就叫反碼。

補碼:反碼加上1,所獲得的就叫補碼。

(這是學術界的名詞,不要糾結爲何,記住便可)

還以-1爲例,進行一下推導:

把 -1看成 1,原碼是,00000001

把0和1對調,反碼是,11111110

而後加上 1, 補碼是,11111111

因而-1的補碼是,11111111。再使用類庫中的工具類輸出一下-1的二進制形式,發現居然仍是它。這也不是巧合,由於:

在計算機中,負數的二進制就是用它的補碼形式表示的。

這就是官方的說法,總喜歡整一些名詞來把你們弄得一懵一懵的。

下面就站在我的角度,以最「土鱉」的方式來揭祕。

首先,-1的二進制是11111111這種形式一會兒確實不容易接受。

反卻是把-1的二進制假設爲10000001更容易讓人接受,由於與它對應的1的二進制是00000001。

這樣從數值的大小上(即絕對值)來看都是1,從符號上來看一個是1一個是0剛好表示一負一正,簡直「堪稱完美」。

那爲何這種假設的形式倒是錯的呢?

由於從十進制的角度來講,1 + (-1) = 0。

再按假設的形式把它們轉換爲對應的二進制,

00000001 + 10000001 = 10000010,

依照假設,這個結果的值是-2。

可見,一個是0,一個是-2,這顯然是不對的。雖然是採用不一樣的進制,但結果應該是同樣的纔對。

很顯然,二進制這種計算方式的結果是錯誤的,錯誤的緣由是,-1的二進制形式不能按照咱們假設的那種方式進行。

那-1的二進制應該按什麼邏輯去計算呢?相信你已經猜到了。

由於,-1 = 0 - 1,因此,

-1 = 00000000 - 00000001 = 11111111。

所以,-1的二進制就是11111111。這樣一來,

-1 + 1 = 11111111 + 00000001 = 00000000 = 0。

這樣是否是一會兒就明白了-1的二進制爲何全是1了。由於這種形式知足了數值計算上的須要。

同理能夠算下-2的二進制,

-2 = -1 - 1 = 11111111 - 00000001 = 11111110。

其實原碼/反碼/補碼之間的轉換關係也是基於正數和負數的和爲零而設計出來的。仔細體會下即可明白。

可見,官方角度和我的角度的本質是同樣的,只不過一個陽春白雪、一個下里巴人。

這讓我想起來了雅和俗,不少人標榜着追求雅,其實他們須要的偏偏是俗。

下面是一些正數和對應負數的例子:

2,00000010
-2,11111110

5,00000101
-5,11111011

127,01111111
-127,10000001

能夠看到十進制數的和是0,對應二進制數的和也是0。

這纔是正確的負數的二進制表示,雖然看起來的跟感受起來的不太同樣。

就十進制來講,當位數固定後,全部位置上都是9時,數值達到最大,如最大的四位數就是9999。

對於二進制來講也是同樣的,除去最高位0表示正數外,剩餘的位置所有是1時,數值達到最大,如最大的八位數就是01111111,對應的十進制數就是127。

一個字節的長度就是8位,所以一個字節能表示的最大正數就是127,即一個0帶着7個1,這是正向的邊界值了。

經過觀察負數,除去最高位1表示負數外,後面7位所有爲0時,應該是負數的最小值,即一個1帶着7個0,對應的十進制數是-128,這是負向的邊界值了。

並且正向和負向的邊界值是有關係的,你發現了嗎?就是正向邊界值加上1以後的相反數即爲負向邊界值。

二進制的常規操做

這些內容應該都很是熟悉了,瞄一眼便可。

位操做

與(and):

1 & 1 -> 1
0 & 1 -> 0
1 & 0 -> 0
0 & 0 -> 0

或(or):

0 | 0 -> 0
0 | 1 -> 1
1 | 0 -> 1
1 | 1 -> 1

非(not):

~0 -> 1
~1 -> 0

異或(xor):

0 ^ 1 -> 1
1 ^ 0 -> 1
0 ^ 0 -> 0
1 ^ 1 -> 0

移位操做

左移(<<):

左邊丟棄(符號位照樣丟棄),右邊補0。

移完後,最高位是0爲正數,是1爲負數。

左移一位至關於乘2,二位至關於乘4,以此類推。

當左移一個週期時,回到原點。即至關於不移。

超過一個週期後,把週期部分除掉,移動剩下的。

移動的位數和二進制自己的長度相等時,稱爲週期。如8位長度的二進制移動8位。

右移(>>):

右邊丟棄,正數左邊補0,負數左邊補1。

右移一位至關於除2,二位至關於除4,以此類推。

在四捨五入時,正數選擇舍,負數選擇入。

正數右移從都丟棄完開始日後數值都是0,由於從左邊補進來的都是0,直到到達一個週期時,回到原點,即回到原來的數值。至關於不移。

負數右移從都丟棄完開始日後數值都是-1,由於從左邊補進來的都是1,直到到達一個週期時,回到原點,即回到原來的數值。至關於不移。

超過一個週期後,把週期部分除掉,移動剩下的。

無符號右移(>>>):

右邊丟棄,不管正數仍是負數左邊都是補0。

所以對於正數來講和右移(>>)沒有什麼差異。

對於負數來講會變成正數,就是使用原來的補碼形式,丟棄右邊後看成正數來計算。

爲何沒有無符號左移呢?

由於左移時,是在右邊補0的,而符號位是在最左邊的,右邊補的東西是影響不到它的。

可能有人會想,到達一個週期後,再移動的話不就影響到了嘛,哈哈,在一個週期的時候是會進行歸零的。

二進制的伸/縮

如下內容都假定高位字節在前低位字節在後的順序。

伸:

如把一個字節伸長爲兩個字節,則須要填充高位字節。(等於把byte類型賦給short類型)

其實就是這個字節原樣不動,在它的左邊再接上一個字節。

此時符號和數值大小都保持不變。

正數符號位是0,伸長時高位字節填充0。

00000110 -> 00000000,00000110

負數符號位是1,伸長時高位字節填充1。

11111010 -> 11111111,11111010

縮:

把兩個字節壓縮爲一個字節,須要截斷高位字節。(等於把short類型強制賦給byte類型)

其實就是左邊字節直接丟棄,右邊字節原樣不動的保留。

此時符號和數值大小均可能發生改變。

若是壓縮後的字節仍能放得下這個數,則符號和數值大小都保持不變。

具體來講就是若是正數的高位字節全是0,同時低位字節的最高位也是0。或負數的高位字節全是1,同時低位字節的最高位也是1。截斷高位字節不會對數形成影響。

00000000,00001100 -> 00001100

11111111,11110011 -> 11110011

若是壓縮後的字節放不下這個數,則數值大小必定改變。

具體說就是若是正數的高位字節不全是0,負數的高位字節不全是1,截斷高位字節確定會對數的大小形成影響。

至於符號是否改變取決於原符號位和壓縮後的符號位是否同樣。

例如,壓縮後大小發生改變,符號不變的以下:

00001000,00000011 壓縮爲 00000011,仍是正數
11011111,11111101 壓縮爲 11111101,仍是負數

例如,壓縮後大小和符號都發生改變的以下:

00001000,10000011 壓縮爲 10000011,正數變負數。
11011111,01111101 壓縮爲 01111101,負數變正數。

整數的序列化和反序列化

通常來講,一個int類型是由四個字節組成的,在序列化時,須要將這四個字節一一拆開,按順序放入到一個字節數組中。

在反序列化時,從字節數組中拿出這四個字節,把它們按順序接在一塊兒,從新解釋爲一個int類型的數字,結果應該保持不變。

在序列化時,主要用到的就是移位和壓縮。

首先將要拆出來的字節移到最低位(即最右邊),而後強制轉換爲byte類型便可。

假若有一個int類型數字以下:

11111001,11001100,10100000,10111001

第一步,右移24位並只保留最低八位,

byte b3 = (byte)(i >> 24);

11111111,11111111,11111111,11111001

11111001

第二步,右移16位並只保留最低八位,

byte b2 = (byte)(i >> 16);

11111111,11111111,11111001,11001100

11001100

第三步,右移8位並只保留最低八位,

byte b1 = (byte)(i >> 8);

11111111,11111001,11001100,10100000

10100000

第三步,右移0位並只保留最低八位,

byte b0 = (byte)(i >> 0);

11111001,11001100,10100000,10111001

10111001

這樣就產生了四個字節,把它們放入字節數組就能夠了。

byte[] bytes = new byte[]{b3, b2, b1, b0};

在反序列化時,主要用到的就是伸長和移位。

首先從字節數組中拿出一個字節,將它轉換爲int類型,而後再處理符號問題,接着再左移到適合位置。

第一步:

取出第一個字節,

11111001

而後伸長爲int,

11111111,11111111,11111111,11111001

由於它的符號位就表示了原來整數的符號位,所以不用處理符號,直接左移24位,

11111001,00000000,00000000,00000000

第二步:

取出第二個字節,

11001100

而後伸長爲int,

11111111,11111111,11111111,11001100

由於它的符號位是處在原來整數的中間位置的,所以它不表示符號而表示數值,須要處理符號位,就是執行一個與操做,

以下,上面兩行相與獲得第三行,

11111111,11111111,11111111,11001100
00000000,00000000,00000000,11111111

00000000,00000000,00000000,11001100

接着左移16位

00000000,11001100,00000000,00000000

第三步,

取出第三個字節,

10100000

而後伸長爲int,

11111111,11111111,11111111,10100000

而後處理符號位,

00000000,00000000,00000000,10100000

接着左移8位,

00000000,00000000,10100000,00000000

第四步,

取出第四個字節,

10111001

而後伸長爲int,

11111111,11111111,11111111,10111001

而後處理符號位,

00000000,00000000,00000000,10111001

接着左移0位,

00000000,00000000,00000000,10111001

這樣四步就產生了四個結果,以下:

11111001,00000000,00000000,00000000
00000000,11001100,00000000,00000000
00000000,00000000,10100000,00000000
00000000,00000000,00000000,10111001

能夠看到四個字節都已經位於本身應該在的位置上了。

最後來一個加法操做就能夠了,其實或操做也是能夠的。

i = i4 + i3 + i2 + i0

i = i4 | i3 | i2 | i0

這樣咱們就將字節數組中的四個字節合成爲一個int類型的數字了。

模擬實現無符號數

無符號數,即最高位不是符號位而是數值位。

有一些語言如Java不支持無符號數,因此須要使用有符號數來模擬實現。

由於同一個類型做爲無符號數時的範圍會大於做爲有符號數時的範圍,所以會用更長的類型存放短類型的無符號數。

如byte類型是一個字節,做爲有符號數時範圍是-128到127,做爲無符號數時範圍是0到255,因此至少須要用兩個字節的short類型來存放。

處理方法很簡單,只需兩步,伸長和處理符號位。

假若有一個字節是,10101011,這是一個byte類型的負數。

第一步,伸長,此時變成兩個字節了,但仍是一個負數

11111111,10101011

第二步,處理符號,即執行一個與操做

11111111,10101011
00000000,11111111

00000000,10101011

這就已經處理完了,由一個字節的負數變成了兩個字節的正數。

其實就是將原來的字節前面(即左邊)接上去一個全0的字節。

當byte做爲無符號數,取到最大值255時,二進制是這樣的

00000000,11111111

此時也只不過纔剛剛使用完低位置。

所以使用長類型表示短類型的無符號數,對長類型的字節利用效率最高也就百分之五十了。

對於這種狀況,在序列化時,其實只需寫入低半部分的字節便可。

在反序列化時,一是要用長類型來承接,二是全部字節都要處理符號,做爲無符號數對待。

PS:此次算是認認真真的複習了十年前在大學裏的專業課基礎知識。

其實我是在寫「品Spring」系列文章時,發現最好能熟悉Java的字節碼(.class)文件內部結構。

在嘗試解析字節碼文件時,發現它裏面存儲的都是無符號數,因此須要寫一個把字節數組反序列化爲無符號數的工具。

在寫工具時看了一點JDK相關部分的源碼,就索性把二進制的基本知識和操做都親自寫代碼測試了一遍。

因而就整理出了這篇文章,呵呵。

文中全部代碼示例:
https://github.com/coding-new-talking/java-code-demo.git

(END)

相關文章
相關標籤/搜索