當讀寫二進制文件,或者要把非標準長度的整數與標準長度的整數互相轉換時,就要用到大量的位操作,雖然看起來很簡單,實際上里面卻有很多細節很容易出錯。首先,Java有些標準跟C/C++是不同的:1、Java采用高字節在前的方式讀寫數據,例如要把一個4字節的int數值寫入文件時,它是按照從高字節到低字節的順序寫入的,讀取的時候也是這樣讀出來。而C/C++則采用平臺相關的方式,在Windows平臺采用低字節在前的方式,在Linux/Unix平臺則采用高字節在前的方式。如果Java要讀取C/C++創建的二進制文件,就要注意這個問題,最好先搞清楚原來的文件是采用哪種方式創建的。網絡通信也要注意。2、Java沒有無符號數,無論byte,short,int,long都是有符號整數,而C/C++有個unsigned關鍵字可以設置一個數值為無符號數。3、Java的整數基本數據類型就是byte,short,int,long這幾個,長度分別為1,2,4,8字節,C/C++可以用typedef定義各種數據類型。第二,Java是采用補碼來存放整數的。
有時候覺得補碼的定義有些奇怪,實際上可以這樣理解:把一個整數從0一直往上加1,加到溢出就變成了負數的最小值,然后再繼續加1,最后又能回到0,實際上就是一個輪回。例如一個byte類型的整數,一共有8位,能表示256個數值,采用補碼的話數值范圍就是-128~127,表示方法如下:0 0000 00001 0000 0001..126 0111 1110127 0111 1111-128 1000 0000-127 1000 0001..-1 1111 11110 0000 0000第三、不同長度的整數轉換。如果是從較短的數轉成較長的數,很簡單,如果是正數就在高字節補0,如果是負數就在高字節補1。 例如byte的127轉為short的127:byte:0111 1111short:0000 0000 0111 0111byte的-127轉為short的-127byte:1000 0001short:1111 1111 1000 0001如果是從較長的數轉成較短的數,實際上就是把高位都截斷了,所以轉出來的數值可能完全不是一回事了。例如short的256轉為byte:short:0000 0001 0000 0000byte: 0000 0000把256變成了0short的-255轉成byte:short:1111 1111 0000 0001byte:0000 0001把-255變成了1第四、位運算操作符及它們的優先級Java的位運算操作符包括:~非,|按位或,&按位與,^按位異或,<<左移,>>右移,>>>右移左側補0各種運算符的優先級如下表所示:優先級 | 運算符 | 結合性 |
1 | () [] . | 從左到右 |
2 | ! +(正) -(負) ~ ++ -- | 從右向左 |
3 | * / % | 從左向右 |
4 | +(加) -(減) | 從左向右 |
5 | << >> >>> | 從左向右 |
6 | < <= > >= instanceof | 從左向右 |
7 | == != | 從左向右 |
8 | &(按位與) | 從左向右 |
9 | ^ | 從左向右 |
10 | | | 從左向右 |
11 | && | 從左向右 |
12 | || | 從左向右 |
13 | ?: | 從右向左 |
14 | = += -= *= /= %= &= |= ^= ~= <<= >>= >>>= | 從右向左 |
根據該表可以看到,位運算操作符的優先級各有不同,分別為:1、~2、>> << >>>3、&4、^5、|另外需要特別注意的是,除了~,其他位運算操作的優先級都低于加減,所以要記得以下語句是返回32而不是7!1<<2+3還有就是&、^、|的優先級都是低于邏輯操作符的,因此下面的語句會編譯出錯,幸好Java不像C那樣對所有大于1的值都認為是真,否則下面的語句也能編譯通過,但可能與你的意圖不太一樣,可能調試半天才發現。if(3&1>0)如果記不清楚,還是按照你的意圖加上括號最保險。第五、字節數組與整數之間的轉換為了把一個整數存入文件,或者從文件中讀取一個整數,需要經常在字節數組和整數之間轉換,這個過程要用到大量的位運算。首先需要記住的是,在參與所有運算前,Java都會把byte、short類型的值都轉換成int,然后再對轉換后的int進行操作。例如下面的語句會編譯出錯:byte a=10,b=20,c;
c=a+b;
因為a和b在相加前都被轉成了int,最后得到的結果是個int類型的值,如果要賦給byte類型的c,必須顯式地進行類型轉換,即把第二句改為:c=(byte)(a+b)
這一點很關鍵,因為對于一個最高位為1的byte類型的整數(負數),在運算之前它會被強制轉換成int類型,根據上面所說的第三點,其實就是往前面的三個高字節補上1,這樣一來,它在參與位運算的過程中,就不僅僅是它本身的8個bit參與了,實際上連前3個字節的24個bit(均為1)也參與了。例如有一個整數i=1082163328,它的二進制表示為:01000000 10000000 10000000 10000000分為4個字節存儲,除了第一個字節是正數外,其余3個字節均為負數。假如用a代表最高字節的值,用b代表其他三個字節的值,如果按照通常的理解,你可能會這樣得到i的值:i=(a<<24)+(b<<16)+(b<<8)+b
如果a和b都是正數,上面的等式是成立的,但是在這個例子里,卻是錯的,因為上式中的a和b都已經被強制轉換成了int類型再參加運算,實際上a=00000000 00000000 00000000 01000000b=11111111 11111111 11111111 10000000i=01000000 00000000 00000000 00000000+11111111 10000000 00000000 00000000+11111111 11111111 10000000 00000000+11111111 11111111 11111111 10000000 最后得到的結果是1065320320,不是原來的值了。為了不讓byte在強制轉換成int的過程加入了我們不想要的高位1,我們需要把它跟0xff進行與操作,i的值應該這樣運算:i = ( ( a& 0xff ) << 24 ) +( ( b & 0xff ) << 16 ) + ( ( b & 0xff ) << 8 ) + ( b & 0xff )
注意,因為&和<<的優先級都低于+,所以上面的括號是不能少的。不過由于跟0xff與操作之后,其余24位都變成了0,因此可以把+改為|操作,因為任何值與0進行或操作都得到本身:i = ( a & 0xff ) << 24 | ( b & 0xff ) << 16 | ( b & 0xff ) << 8 | ( b & 0xff )
由于<<的優先級高于|,所以省了一些括號。最高字節可以不與0xff進行與操作,因為它轉換成int后左邊增加的3個字節都在左移24位時被去掉了:i = a << 24 | ( b & 0xff ) << 16 | ( b & 0xff ) << 8 | ( b & 0xff )
把int轉為字節數組的時候比較簡單,直接右移截斷即可:byte[] b = new byte[4];
b[0] = (byte) (i >> 24);
b[1] = (byte) (i >> 16);
b[2] = (byte) (i >> 8);
b[3] = (byte) i;
第六、非標準長度整數的存儲和讀取假如有兩個變量,他們的值可以用12個bit來表示,如果我們用16bit的short類型來表示一個變量,那么兩個變量就需要4個字節,而實際上它們只需要3個字節就能表示出來,如果存儲空間比較有限,寫入文件時可以把它們存放在3個字節里面,但是讀寫過程就需要進行轉換。在內存里,它們都是標準的數據類型:short a,b;
寫入文件時,我們用第一個字節和第二個字節的前半部分來表示a,把第二個字節的后半部分和第三個字節來表示b,即:1:xxxx xxxx2:xxxx yyyy3:yyyy yyyyx和y都表示一個bit,分別用來存放a和b。寫入時先把a和b轉為字節數組:byte[] out = new byte[3];
out[0] = (byte) ( a >> 4 );//把a的高8位放在第一個字節
out[1] = (byte) ( a << 4 );//先把a左移四位,在右邊補上4個0,第二個字節的高4位就是a的低4位了,第二個字節的高4位已經生成,低4位還是0
out[1] |= (byte) ( b >> 8 & 0x0f );//b右移8位,并與0x0f進行與操作,實際上就只保留了b的高4位,并且是在字節的低4位上,跟第二步得到的字節進行或操作,就生成了第二個字節
out[2] = (byte) b;//把b的高4位截斷就得到了低8位
然后再把這個字節數組寫入文件,就可以用3個字節表示兩個整數了。讀取:a =(short)( (out[0] & 0xff) << 4 | ( out[1] & 0xf0 )>>4);
b = (short)((out[1] & 0x0f) << 8 | ( out[2] & 0xff));