1. 程式人生 > >Java之位元組&0xff、原碼、反碼、補碼、位移、

Java之位元組&0xff、原碼、反碼、補碼、位移、

最近在做編碼轉換時,發現一段別人寫的程式碼:

	public static String byte2hexString(byte[] bytes) {
		StringBuffer buf = new StringBuffer(bytes.length * 2);
		for (int i = 0; i < bytes.length; i++) {
			int c = bytes[i] & 0xFF;
			if (c < 0x10) { // 等價於c < 16 
				buf.append("0");
			}
			buf.append(Long.toString((int) bytes[i] & 0xFF, 16));
		}
		return buf.toString();
	}

為什麼要將bytes[i]&0xFF再複製給int型別呢?為此,我們先來了解一下原碼、反碼、補碼的基本知識。

在32位的電腦中數字都是以32格式存放的,如果是一個byte(8位)型別的數字,他的高24位裡面都是隨機數字,低8位才是實際的資料java.lang.Integer.toHexString() 方法的引數是int(32位)型別,如果輸入一個byte(8位)型別的數字,這個方法會把這個數字的高24為也看作有效位,這就必然導致錯誤,使用& 0XFF操作,可以把高24位置0以避免這樣錯誤的發生。

一. 機器數和真值

在學習原碼, 反碼和補碼之前, 需要先了解機器數和真值的概念.

1、機器數

一個數在計算機中的二進位制表示形式,  叫做這個數的機器數。機器數是帶符號的,在計算機用一個數的最高位存放符號, 正數為0, 負數為1(Java沒有無符號數,全部都是有符號數,需要用最高位表示正負;但C語言中有無符號數).

比如,十進位制中的數 +3 ,計算機字長為8位,轉換成二進位制就是00000011。如果是 -3 ,就是 10000011 。

那麼,這裡的 00000011 和 10000011 就是機器數。

2、真值

因為第一位是符號位,所以機器數的形式值就不等於真正的數值。例如上面的有符號數 10000011,其最高位1代表負,其真正數值是 -3 而不是形式值131(10000011轉換成十進位制等於131)。所以,為區別起見,將帶符號位的機器數對應的真正數值稱為機器數的真值。

例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

二. 原碼, 反碼, 補碼的基礎概念和計算方法.

在探求為何機器要使用補碼之前, 讓我們先了解原碼, 反碼和補碼的概念.對於一個數, 計算機要使用一定的編碼方式進行儲存. 原碼, 反碼, 補碼是機器儲存一個具體數字的編碼方式. 

1. 原碼

原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進位制:

[+1] = 0000 0001

[-1] = 1000 0001

第一位是符號位. 因為第一位是符號位, 所以8位二進位制數的取值範圍就是:

[1111 1111 , 0111 1111] 

[-127 , 127]

原碼是人腦最容易理解和計算的表示方式.

2. 反碼

反碼的表示方法是:

正數的反碼是其本身

負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反.

[+1] = [00000001] = [00000001]

[-1] = [10000001] = [11111110]

可見如果一個反碼錶示的是負數, 人腦無法直觀的看出來它的數值. 通常要將其轉換成原碼再計算.

3. 補碼

補碼的表示方法是:

正數的補碼就是其本身

負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)

[+1] = [00000001] = [00000001] = [00000001]

[-1] = [10000001] = [11111110] = [11111111]

對於負數, 補碼錶示方式也是人腦無法直觀看出其數值的. 通常也需要轉換成原碼,再計算其數值.

三. 為何要使用原碼, 反碼和補碼

在開始深入學習前, 我的學習建議是先"死記硬背"上面的原碼, 反碼和補碼的表示方式以及計算方法.

現在我們知道了計算機可以有三種編碼方式表示一個數. 對於正數因為三種編碼方式的結果都相同:

[+1] = [00000001] = [00000001] = [00000001]

所以不需要過多解釋. 但是對於負數:

[-1] = [10000001] = [11111110] = [11111111]

可見原碼、反碼和補碼是完全不同的. 既然原碼才是被人腦直接識別並用於計算表示方式, 為何還會有反碼和補碼呢?

首先, 因為人腦可以知道第一位是符號位, 在計算的時候我們會根據符號位, 選擇對真值區域的加減. (真值的概念在本文最開頭). 但是對於計算機, 加減乘數已經是最基礎的運算, 要設計的儘量簡單.計算機辨別"符號位"顯然會讓計算機的基礎電路設計變得十分複雜!於是人們想出了將符號位也參與運算的方法. 我們知道, 根據運演算法則減去一個正數等於加上一個負數, 即: 1-1 = 1 + (-1) = 0 ,所以機器只有加法而沒有減法, 這樣計算機運算的設計就更簡單了.

於是人們開始探索 將符號位參與運算, 並且只保留加法的方法. 首先來看原碼:

計算十進位制的表示式: 1-1=0

1 - 1 = 1 + (-1) = [00000001] + [10000001] = [10000010] = -2 

如果用原碼錶示, 讓符號位也參與計算, 顯然對於減法來說, 結果是不正確的.這也就是為何計算機內部不使用原碼錶示一個數.

為了解決原碼做減法的問題, 出現了反碼:

計算十進位制的表示式: 1-1=0

1 - 1 = 1 + (-1) = [0000 0001] + [1000 0001]= [0000 0001] + [1111 1110] = [1111 1111] = [1000 0000]= -0

發現用反碼計算減法, 結果的真值部分是正確的. 而唯一的問題其實就出現在"0"這個特殊的數值上. 雖然人們理解上+0和-0是一樣的, 但是0帶符號是沒有任何意義的. 而且會有[0000 0000]和[1000 0000]兩個編碼表示0.

於是補碼的出現, 解決了0的符號以及兩個編碼的問題:

1-1 = 1 + (-1) = [0000 0001] + [1000 0001] = [0000 0001] + [1111 1111] = [0000 0000]=[0000 0000]

這樣0用[0000 0000]表示, 而以前出現問題的-0則不存在了.而且可以用[1000 0000]表示-128

(-1) + (-127) = [1000 0001] + [1111 1111] = [1111 1111] + [1000 0001] = [1000 0000]

-1-127的結果應該是-128, 在用補碼運算的結果中, [1000 0000] 就是-128. 但是注意因為實際上是使用以前的-0的補碼來表示-128, 所以-128並沒有原碼和反碼錶示.(對-128的補碼錶示[1000 0000]補算出來的原碼是[0000 0000], 這是不正確的)

使用補碼, 不僅僅修復了0的符號以及存在兩個編碼的問題, 而且還能夠多表示一個最低數. 這就是為什麼8位二進位制, 使用原碼或反碼錶示的範圍為[-127, +127], 而使用補碼錶示的範圍為[-128, 127].

因為機器使用補碼, 所以對於程式設計中常用到的32位int型別, 可以表示範圍是: [-231, 231-1] 因為第一位表示的是符號位.而使用補碼錶示時又可以多儲存一個最小值.

八位二進位制數能表示數的範圍以及原碼、反碼和補碼含義

首先八位二進位制數0000 0000 ~1111 1111,一共可以表示2^8=256位數,如果表示無符號整數可以表示0~255。計算方法就是二進位制與十進位制之間的轉換。

11111111

2^7 = 128
2^6 = 64
2^5 = 32
2^4 = 16
2^3 = 8
2^2 = 4
2^1 = 2
2^0 = 1


128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

64 + 32 + 16 + 8 + 4 + 2 + 1 = 127

如果想要表示有符號整數,就要將最前面一個二進位制位作為符號位,即0代表正數,1代表負數,後面7位為數值域,這就是原碼定義。這樣在現實生活中完全沒有問題,但在計算機中就出現了問題。

  1. 數的表示: 
    在原碼中,0的表示有兩種(+0)0000 0000、(-0)1000 0000,這樣就產生了編碼對映的不唯一性,在計算機上就要區分辨別。然而+0、-0卻沒有什麼現實意義。
  2. 數的運算: 
    為了解決上述數的表示問題,我們可以強制把轉換後的10000000強制認定為-128。但這又出現了一個新的問題就是數的運算。數學上,1+(-1)=0,而在二進位制中00000001+10000001=10000010,換算成十進位制為-2。顯然出錯了。所以原碼的符號位不能直接參與運算,必須和其他位分開,這就增加了硬體的開銷和複雜性。 
    這個時候就要引入補碼,
  3. 補碼錶示法規定:1)正數的補碼與其原碼相同;2)負數的補碼是在其反碼的末位加1。
  4. 反碼定義為:1)正數的反碼與其原碼相同;2)負數的反碼是對其原碼逐位取反,但符號位除外。
    • 但為什麼要引入補碼呢?
    • 以及負數補碼定義為什麼是原碼取反加一?

先解決第一個問題,引入補碼是為了解決計算機中數的表示和數的運算問題,使用補碼,可以將符號位和數值域統一處理,即引用了模運算在數理上對符號位的自動處理,利用模的自動丟棄實現了符號位的自然處理,僅僅通過編碼的改變就可以在不更改機器物理架構的基礎上完成的預期的要求。

要解決第二個問題同時理解補碼,首先要理解“模”,模的概念可以幫助理解補數和補碼。 

“模”是指一個計量系統的計數範圍。如時鐘、計算機也可以看成一個計量機器,它也有一個計量範圍,即都存在一個“模”。例如: 時鐘的計量範圍是0~11,模=12。表示n位的計算機計量範圍是0~2^(n)-1,模=2^(n)

“模”實質上是計量器產生“溢位”的量,它的值在計量器上表示不出來,計量器上只能表示出模的餘數。任何有模的計量器,均可化減法為加法運算。

例如:假設當前時針指向10點,而準確時間是6點,調整時間可有以下兩種撥法:一種是倒撥4小時,即:10-4=6;另一種是順撥8小時:10+8=12+6=6 
在以12模的系統中,加8和減4效果是一樣的,因此凡是減4運算,都可以用加8來代替。對“模”而言,8和4互為補數。實際上以12模的系統中,11和1,10和2,9和3,7和5,6和6都有這個特性。共同的特點是兩者相加等於模

對於計算機,其概念和方法完全一樣。n位計算機,設n=8, 所能表示的最大數是11111111,若再加1成為100000000(9位),但因只有8位,最高位1自然丟失。又回了00000000,所以8位二進位制系統的模為2^8。在這樣的系統中減法問題也可以化成加法問題,只需把減數用相應的補數表示就可以了。把補數用到計算機對數的處理上,就是補碼。

對一個正數的原碼取反加一,得到這個正數對應負數的補碼。例如~6=-7,而且加一之後會多出一個八進位制補碼1000 0000,而這個補碼就對應著原碼1000 0000,數字位同時當做符號位即-128。

根據以上內容我們就可以來解釋八位二進位制數的表示範圍

八位二進位制正數的補碼範圍是0000 0000 -> 0111 1111 即0 -> 127,

負數的補碼範圍是正數的原碼0000 0000 -> 0111 1111 取反加一(也可以理解為負數1000 0000 -> 1111 1111化為反碼末尾再加一)。 

所以得到 1 0000 0000 -> 1000 0001,1000 0001作為補碼,其原碼是1111 1111(-127),依次往前推,可得到-1的補碼為1111 1111,那麼補碼0000 0000的原碼是1000 0000符號位同時也可以看做數字位即表示-128

這也解釋了為什麼127(0111 1111)+1(0000 0001)=-128(0000 0000)。

總結

在計算機中資料用補碼錶示,利用補碼統一了符號位與數值位的運算,同時解決了+0、-0問題,將空出來的二進位制原碼1000 0000表示為-128,這也符合自身邏輯意義的完整性。因此八位二進位制數表示範圍為-128~+127。



回到最初的問題,為什麼要&0xFF

拿到的檔案流轉成byte陣列,難道我們關心的是byte陣列的十進位制的值是多少嗎?我們關心的是其背後二進位制儲存的補碼

所以大家應該能猜到為什麼byte型別的數字要&0xff再賦值給int型別,其本質原因就是想保持二進位制補碼的一致性。

當byte要轉化為int的時候,高的24位必然會補1,這樣,其二進位制補碼其實已經不一致了,&0xff可以將高的24位置為0,低8位保持原樣(32位系統編譯器,int佔4個位元組)。這樣做的目的就是為了保證二進位制資料的一致性。

當然,保證了二進位制資料性的同時,如果二進位制被分別當作byte和int來解讀,其10進位制的值必然是不同的,因為符號位位置已經發生了變化。

public static void main(String[] args) {
        byte[] a = new byte[10];
        a[0]= -127;
        System.out.println(a[0]);
        int c = a[0]&0xff;
        System.out.println(c);
    }

a[0] & 0xff = 1_11111111_11111111_11111111_10000001 & 1_1111111 = 00000000_00000000_0000000010000001,這個值就是129,所以c的輸出的值就是129。

有人問為什麼a[0]不是8位而是32位,因為當系統檢測到byte轉化成int時,會將byte的記憶體空間高位補1(也就是按符號位補位)擴充到32位,再參與運算。上面的0xff其實是int型別的字面量值,所以可以說byte與int進行運算

四、 左移運算子  

左移運算子<<使指定值的所有位都左移規定的次數。 
1)它的通用格式如下所示:  value << num  
num 指定要移位值value 移動的位數。  
左移的規則只記住一點:丟棄最高位,0補最低位  
如果移動的位數超過了該型別的最大位數,那麼編譯器會對移動的位數取模。如對int型移動33位,實際上只移動了33%32=1位。 
2)運算規則  

按二進位制形式把所有的數字向左移動對應的位數,高位移出(捨棄),低位的空位補零。  當左移的運算數是int 型別時,每移動1位它的第31位就要被移出並且丟棄;  當左移的運算數是long 型別時,每移動1位它的第63位就要被移出並且丟棄。  當左移的運算數是byte 和short型別時,將自動把這些型別擴大為 int 型。 

3)數學意義  
在數字沒有溢位的前提下,對於正數和負數,左移一位都相當於乘以2的1次方,左移n位就相當於乘以2的n次方 
4)計算過程:  
例如:3 <<2(3為int型)  

1)把3轉換為二進位制數字0000 0000 0000 0000 0000 0000 0000 0011, 

 2)把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位,  3)在低位(右側)的兩個空位補零。則得到的最終結果是0000 0000 0000 0000 0000 0000 0000 1100,  

轉換為十進位制是12。  
移動的位數超過了該型別的最大位數,  
如果移進高階位(31或63位),那麼該值將變為負值。下面的程式說明了這一點: Java程式碼  
// Left shifting as a quick way to multiply by 2.    public class MultByTwo {    
public static void main(String args[]) {       int i;    
   int num = 0xFFFFFFE;        for(i=0; i<4; i++) {           num = num << 1;     
     System.out.println(num);       }    }    }  
該程式的輸出如下所示: 
536870908  1073741816  2147483632  -32  

注:n位二進位制,最高位為符號位,因此表示的數值範圍-2^(n-1) ——2^(n-1) -1,所以模為2^(n-1)。 

五、 右移運算子 

右移運算子<<使指定值的所有位都右移規定的次數。  

1)它的通用格式如下所示:  value >> num  

num 指定要移位值value 移動的位數。  
右移的規則只記住一點:符號位不變,左邊補上符號位 
2)運算規則:  
按二進位制形式把所有的數字向右移動對應的位數,低位移出(捨棄),高位的空位補符號位,即正數補零,負數補1  
當右移的運算數是byte 和short型別時,將自動把這些型別擴大為 int 型。  
例如,如果要移走的值為負數,每一次右移都在左邊補1,如果要移走的值為正數,每一次右移都在左邊補0,這叫做符號位擴充套件(保留符號位)(sign extension ),在進行右移 
操作時用來保持負數的符號。 
3)數學意義  
右移一位相當於除2,右移n位相當於除以2的n次方。 
4)計算過程  
11 >>2(11為int型)  

1)11的二進位制形式為:0000 0000 0000 0000 0000 0000 0000 1011  

2)把低位的最後兩個數字移出,因為該數字是正數,所以在高位補零。  

3)最終結果是0000 0000 0000 0000 0000 0000 0000 0010。  轉換為十進位制是3。 

35 >> 2(35為int型)  
35轉換為二進位制:0000 0000 0000 0000 0000 0000 0010 0011  
把低位的最後兩個數字移出:0000 0000 0000 0000 0000 0000 0000 1000  轉換為十進位制: 8 
5)在右移時不保留符號的出來  
右移後的值與0x0f進行按位與運算,這樣可以捨棄任何的符號位擴充套件,以便得到的值可以作為定義陣列的下標,從而得到對應陣列元素代表的十六進位制字元。  例如  
Java程式碼  
public class HexByte {    
public static public void main(String args[]) {    char hex[] = {    
'0', '1', '2', '3', '4', '5', '6', '7',     '8', '9', 'a', 'b', 'c', 'd', 'e', 'f''     };    
byte b = (byte) 0xf1;     
System.out.println("b = 0x" + hex[(b >> 4) & 0x0f] + hex[b & 0x0f]);    }    }   
(b >> 4) & 0x0f的運算過程:  b的二進位制形式為:1111 0001  4位數字被移出:0000 1111  按位與運算:0000 1111  轉為10進位制形式為:15 
b & 0x0f的運算過程:  
b的二進位制形式為:1111 0001  0x0f的二進位制形式為:0000 1111  按位與運算:0000 0001  轉為10進位制形式為:1 所以,該程式的輸出如下:  b = 0xf1 

六、 無符號右移  

無符號右移運算子>>>  它的通用格式如下所示:  value >>> num  
num 指定要移位值value 移動的位數。  
無符號右移的規則只記住一點:忽略了符號位擴充套件,0補最高位  無符號右移運算子>>> 只是對32位和64位的值有意義


int 型別佔據多少位元組?到底是跟編譯器有關?還是系統來決定的?

1. CPU的設計者才不管你在上面跑什麼程式.他們只是按著他們的想法來設計.
而int的大小,至少在C/C++中,標準只說可以由實現者自己定義.至於要不要按機器的字長來設計那就是編譯器設計者的喜好了.除非哪天標準改成int必須是機器的字長....不過C/C++標準中抽象出來的"機器",恐怕沒有字長這個概念吧.

2. 編譯器,簡單的說目前流行的的是32位機,但TC裡的int就是16位的,流行的編譯器(gcc,VC)都是32位。
另外64位機也有了,但上面的int還是可以是32位的。
與此同時,就算在32位機上,也可以做出64位的int來。
所以只要願意,編譯器能決定出int的大小,就算是8位的機器也能搞出64位的來。

3. 一個簡單大家都知道的事實!
trubo C的int是2位元組
vc的int是4位元組

再看java編譯器,無論在什麼機器上,int都是那麼大
所謂跟平臺無關,就是跟機器和作業系統沒有關係!

4. 機器第一作用,編譯器第二作用.
現在新出的機器開始有64位的,編譯器也逐漸的要適應這個改變,由32位向64位過渡.
如果機器是16位的,編譯器強制為32位的話,效率及其低下,沒那家廠商會做如此SB的事情,
我們現在的機器一般都是32位的,編譯器所以一般也是32位,但也支援64位的,
__int64  就是64位元組的,
總之int 只是一個型別,沒有太大的意義,機器的位數才是關鍵所在!
離開機器,說有編譯器決定的人,實在不敢恭維.
難道要在8位機上實現64bit的編譯器?
機器進步了,編譯器跟不上,就要被淘汰,
編譯器超前了,效率低下,也不會有市場,

所以不要單純的討論編譯器或者機器了。
OVER!

參考:

http://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html

http://www.cnblogs.com/think-in-java/p/5527389.html

http://www.cnblogs.com/minggeqiuzhi/archive/2014/07/10/3835883.html

http://blog.csdn.net/zimo2013/article/details/40047695

http://blog.csdn.net/fenzang/article/details/53500852