深入理解Java中的位操作
與、或、非、位移
原碼、反碼、補碼
位元組、位、超區間......
開始本章節之前,我們先思考一個問題:
byte a = 33;
byte b = -3;
若我們輸出a、b的二進位制字串是多少?
答案是這樣的麼?
a->// 00100001
b->// 10100001
當然同學們可能會覺得我既然問了就肯定不是這樣;是吧~彆著急你們試試就知道了。
在Java中輸出一個值對應的二進位制方法有很多,這裡提供一個簡單的方法:
int value = 33;
String bs = String.format("%32s", Integer.toBinaryString(value)).replace(" ", "0");
在方法中是int值,int佔4位元組32位,所以是: “%32s” 若是byte將32改成8即可;當然對於byte你還需要加上**“&0xFF”**來做高位清零操作。
String bs = String.format("%8s", Integer.toBinaryString(value&0xFF)).replace(" ", "0");
基本原則
在Java中是採用的有符號的運算方式,故:高位為符號位,其餘位儲存資料資訊。
簡單來說:
+1 ->// 00000001
-1 ->// 10000001
預設例子中的值都按byte來算,佔8位,減少大家的記憶負擔。
因為byte佔8位,所以有效資料儲存7位,最高位為符號位。int值則是31位儲存資料。
0 代表正數
1 代表負數
上述的-1的表示方法其實並不是機器碼,而是人腦的理解方式。
我們認為+1與-1的差異就是高位不同而已,這是我們基於自然規律來看的;而機器真正儲存的值其實是:11111111;這裡其實就給大家提到了最初的問題。
二進位制的計算規則是: 逢2進1
這個很好理解,因為表示的數字就是:0、1兩個數字,想要表示更大的值就只能往前遞增進步。
在平時生活中是逢10進1;因為咱們有10個數字:9、8、7、6、5、4、3、2、1、0;所以11就是:當為 0|9 增加為10的時候就進一格所以變成: 1|0 ,個位再把剩餘的1補上就是: 1|1 ;所以就是11。
那麼:
1就是:0|0|0|0|0|0|0|1
2就是:0|0|0|0|0|0|1|0
3就是:0|0|0|0|0|0|1|1
4就是:0|0|0|0|0|1|0|0
運演算法則

設
byte a = (byte) 0b01011000; // 88
byte b = (byte) 0b10101000; // -88
int n = 1;
按位與 a & b

輸入2個引數
a、b對應位都為1時,c對應位為1;反之為0。
按位或 a | b **

輸入2個引數
a、b對應位只要有一個為1,c對應位就為1;反之為0。
按位異或 a^b

輸入2個引數
a、b對應位只要不同,則c對應位就為1;反之為0。
按位取反(非)

輸入1個引數
c對應位與輸入引數a完全相反;a對應位為1,則c對應位就為0;a對應位為0,則c對應位就為1。
左移

輸入1個引數a;n = 1
a對應位全部左移動n位得到c;a最左邊的n個位全部丟棄(紅色框),c最右邊n個位補充0(綠色框)。
右移(帶符號)

輸入1個引數b;n = 1
這裡將引數換為b是因為b為負數,第一個位為1
b對應位全部右移動n位得到c;b最右邊n個位全部丟掉(紅色框),c最左邊n個位補充1(綠色框)。
這裡需要注意的是其左邊補充的值取決於b的最高位也就是符號位:符號位是1則補充1,符號位是0則補充0。
右移(無符號)

輸入1個引數b;n = 1
這裡將引數換為b是因為b為負數,第一個位為1
b對應位全部右移動n位得到c;b最右邊n個位全部丟掉(紅色框),c最左邊n個位補充0(綠色框)。
這裡需要注意的是其左邊補充的值永遠為0,不管其最高位(符號位)的值。
進製表示規範
這個小節是插曲,部分同學可能注意到上面寫的進制定義是: 0b01011000 ,部分同學 可能疑惑為什麼不是 0x 之類的。
字首
十進位制:直接寫數字即可
二進位制:0b或0B開頭;如:0b01011000 代表十進位制 88
八進位制:0 開頭;如:0130 代表十進位制 88 (1x64+3x8)
十六進位制:0x或0X開頭;如:0x58 代表 88 (5x16+8)
字尾
0x?? 若小於127 則按byte算,大於則按int型別算
0xFF預設為int型別
若宣告為long新增字尾:L或l:如:0xFFL 或 0xFFl
帶小數的值預設為double型別;如:0.1
若宣告為float新增字尾:f 或 F:如:0.1F
若宣告為double新增字尾:d或D:如:1D
範圍
二進位制:1、0
八進位制:0~7
十進位制:0~9
十六進位制:0~9 + A~F
型別轉換
在上述運演算法則中:兩個不同長度的資料進行 位運算 時,系統會將二者按右端對齊左端補齊,然後進行 位運算 。
設
a 為 int 佔32位
b 為 byte 佔8位
執行: a&b 、a|b 、a^b….等操作時:
若b為正數,則左邊補齊24個0
若b為負數,則左邊補齊24個1
若b = 0b01011000 補齊後:0b 00000000 00000000 00000000 01011000
若b = (byte) 0b10101000 補齊後:0b 11111111 11111111 11111111 10101000
為什麼 b = 0b10101000 需要加上 (byte) 強轉?
因為預設的0b10101000會被理解為:0b 00000000 00000000 00000000 10101000,這個值是一個超byte範圍的int值(正數):168。
當強轉 byte 後高位丟棄,保留低8位,對於byte來說低8中的高位就是符號位;所以運算後就是:-88(byte)。
原碼、反碼、補碼
相信看了上面那麼多的各種規定後,大家有一定的疑問,為什麼正數與負數與大家所想的不大一樣呢?
我相信大家覺得正數負數就是這樣的:
// 錯誤的理解
// 0b01011000 -> 88 : (64+16+8)
// 0b11011000 -> -88 : -(64+16+8)
大家可能會想,正數與負數不就應該只是差符號位的變化麼?
// 正確的理解
// 0b01011000 -> 88 : (64+16+8)
// 0b10101000 -> -88 : -(64+16+8)
0b10101000 : -(64+16+8) ??WTF?? 除了符號位能懂以外請你告訴我是怎麼得出 64、16、8的?
在這裡我們先設兩個基本的概念:
原碼:人所能直接理解的編碼
機器碼:計算機能直接理解的編碼
允許我先說一個小故事 :對於在坐的各位來說計算1-1是非常簡單的,但是對於計算機來說就是計算:00000001 與 10000001 (暫且按8位,原碼)。

計算機需要識別出橙色部分的符號位,然後提取出粉色部分的資料進行計算;這裡有兩個問題:
識別橙色符號位是困難的
若橙色部分是負數則需要增加減法計算模組
但對於計算機來說做加法就夠了,將 1-1換算為:1+(-1) ;OK這一步就是將所有的減法都換算為加法進行計算,減少了減法硬體模組的設計,提升了計算機的硬體利用率。
但是這裡就有一個問題了,既然是將-1當作了一個值來進行運算,那麼必然這個值需要方便做加法才行;按上圖來說我們必不可免的需要去做一次符號位的判斷,然後再做資料位的減法操作,簡單來說還是在做減法。
所以若計算機的機器碼直接採用原碼則會導致硬體資源的設計問題。
有沒有一種辦法將符號位直接儲存到整個結構中,讓計算機在計算過程中不去管所謂的符號位與資料位?有的!就是反碼。
反碼
正數的反碼是其本身
負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反。可以簡單理解為 "~a | 10000000"
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反

如上圖,咱們將 -1 的原碼轉化為了反碼;此時我們使用 反[+1] + 反[-1] 進行一次運算:

此時咱們可以得到一個值x,這個值可以確定的是符號位為1,為負數,後面資料位全部為1;因為此時是 反碼狀態 ,所以要想我們能直接讀取資料是不是應該 轉化為原碼狀態 啊。
// 反碼轉原碼流程就是倒過來,符號位不變,其餘位為取反即可。
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原= [00000001]反 + [11111110]反 = [11111111]反 = [10000000]原 = -0
可以看出我們已經解決好了運算的問題了,計算機只需要按照反碼的方式去計算即可,只需要做加法,不需要做減法就可以運算減法流程。計算完成後對於人腦來說需要將反碼轉化為原碼就是可讀的資料了。
但上述也暴露一個問題:-0 的問題;對於0的表示將會出現兩種情況:
[11111111]反 = [10000000]原 = -0
[01111111]反 = [00000000]原 = +0
也就是出現兩種為0的表示值,-0與+0;但對於我們來說0就是0,不需要做區分。所以又引入了補碼。
補碼
正數與反碼規則一樣無需變化:補碼=反碼=原碼
負數在反碼基礎上保證符號位不變,從右端+1
[+1] = [00000001]原 = [00000001]反 = [00000001]補
[-1] = [10000001]原 = [11111110]反 = [11111111]補

此時若計算機使用補碼直接進行計算會怎樣?

當我們使用補碼計算時,因為末尾的兩位均為1,1+1 = 2;對於二進位制來說滿2進1,所以往前進位1,進位後又遇到 1+1 = 2的情形,所以依次進位,當前位置0。
最終計算後就是: 1 00000000 ,一共9位,因為當前只有8位,所以自然就只剩下: 00000000 。
請注意:在當前運算過程中符號位並無差別也直接當作普通值進行步進運算!
如此我們就完成了整個流程的運算,但你還需注意的是,當前運算後的值是 補碼 ,也就是機器直接操作的編碼;如果要還原為我們可讀的值需要反向轉化為 原碼 。由最初定義可知,正數:原碼=補碼;上述補碼為正數,所以原碼也是:00000000。整個流程如下:
// 補碼計算流程
1 - 1 = 1 + (-1)
= [00000001]原 + [10000001]原
= [00000001]反 + [11111110]反
= [00000001]補 + [11111111]補
= [00000000]補
= [00000000]原
= 0
補碼->原碼
正數的補碼就是原碼
負數:
直接倒敘流程,保證符號位不變右端減1,再保證符號位不變其餘位取反即可
再走一遍補碼流程;補碼的補碼就是原碼(先取反再+1即可)【敲黑板】
思考[10000000]代表什麼?
若是某個計算完成後的補碼值為:10000000 那麼他對應的值是什麼呢?
// 按方案1來看:
[10000000]補 = [11111111]反 = [10000000]原
// 按方案2來看:
[10000000]補 = [11111111]補反 = [10000000]補補 = [10000000]原
可見方案1、方案2都是一樣的,補碼的補碼就是原碼。
[10000000]原 = 是等於0呢?還是-0呢?還是-128呢?
因為我們已經規定了:[00000000]原 = 0;為了充分利用位的儲存區間,所以將:[10000000]原 = -128
一般情況下不會對[10000000]補碼求原碼,因為也沒啥意義~
思考(127、-127)原碼、反碼、補碼是多少?
對於正數:
127 = [01111111]原 = [01111111]反 = [01111111]補
對於負數:
-127 = [11111111]原 = [10000000]反 = [10000001]補
對於計算機來說,其儲存的值 都是補碼 ,所以也就造成了一開始我們提到的問題:為什麼88與-88的二進位制並不只是符號位不同?
再次強調 :計算機儲存的是補碼,為了方便運算;我們想要理解其表示的值需要轉化為原碼。
溢位問題
因為計算機計算過程中不再區別符號位,直接將符號位也納入運算流程中;所以也就可以解釋2個基礎問題:(溢位)
兩個正數相加為負數
兩個負數相加為正數
大家可以分析一下:
88+100
(-66) + (-88)
上述計算在byte變數範圍下進行計算,嘗試分析一下補碼的計算流程。
儲存區間
預設的對於採用補碼的計算機系統而言,其儲存值的有效範圍是:-2^(n-1) ~ 2^(n-1) -1 ;n代表當前的位數。
byte,1位元組,8位:-2^7 ~ 2^7 -1 = -128~127
short,2位元組,16位:-2^15 ~ 2^15 -1 = -32768 ~ 32767
int,4位元組,32位:2^31 ~ 2^31 -1
......
若,我想在byte中儲存超過127的值會怎樣?
設 :
int i = 200
對應補碼為: 0000 0000 0000 0000 0000 0000 1100 1000
因200未超256(2^8)所以依然只會使用到8個位
int i = 200; // 0000 0000 0000 0000 0000 0000 1100 1000 (200)
byte b = (byte) 200; // 1100 1000
當我們將200強轉為byte時高位丟棄僅剩下低8位:1100 1000
如果我們對byte進行輸出會怎樣?
System.out.println(b); // "-56"
首先其直接呼叫的是: public void println(int x) 方法,OK,既然是int輸出為啥不是200?而是-56?
就算有這樣的方法: public void println(byte x) 方法,會輸出200麼?也不會!!
首先對於byte b來說: 1100 1000 這是一個負數的補碼,其原碼流程是:
[1100 1000]補 = [1011 0111] = [1011 1000]原 = -(32+16+8) = -56
這裡有一個有趣的事情,int轉byte時是直接丟掉高位的所有資料:24個0;但byte轉int時,補充高24位時是根據當前的符號位來補充的,若當前符號位是1則添1,若符號位是0則添0;對於byte來說第一位就是符號位,當前的 1100 1000 符號位是**“1”**所以新增的就是24位1。
int c = b; // b -> 1100 1000
// c -> 1111 1111 1111 1111 1111 1111 1100 1000
若直接列印的是byte值,就是-56;上面我們分析1100 1000的原碼時就已經證明了。那麼列印c是不是呢?
對於範圍較少的型別轉換位大型別時不會丟失資料,原來是什麼就是什麼。
OK,就算不是上面那句話,我們來看看:
[1111 1111 1111 1111 1111 1111 1100 1000]補
= [1000 0000 0000 0000 0000 0000 0011 0111]
= [1000 0000 0000 0000 0000 0000 0011 1000]原
= -(32+16+8) = -56
若我們轉換為int時想要還原最初的200這個值該如何辦?
分析上面的補碼,可以看出其與最初的補碼差異僅僅在於左邊24位的不同:
[1111 1111 1111 1111 1111 1111 1100 1000]補 = -56
[0000 0000 0000 0000 0000 0000 1100 1000]補 = 200
那麼我們只需要將前面的24位重置為0即可,這裡就有一個與操作的簡單用法:
/**
*
* 1111 1111 1111 1111 1111 1111 1100 1000 (the int)
* &
* 0000 0000 0000 0000 0000 0000 1111 1111 (the 0xFF)
* =======================================
* 0000 0000 0000 0000 0000 0000 1100 1000 (200)
*/
System.out.println(b & 0xFF); // "200"
在這裡我們做了一次特殊的: b & 0xFF 操作,b 轉換為int之後的值與 0xFF 進行按位與操作。
0xFF = 255 其int原碼為:0000 0000 0000 0000 0000 0000 1111 1111,恰好最後8位為1,其餘24位為0;所以可以用來做高位擦除操作。
這樣的用法可用以儲存超範圍的資料,比如對於檔案的大小來說永遠都是 >= 0,不可能會使用到 < 0 的值,所以對於原始的我們可以根據這個,使用較少的byte表示更多的區間,簡單來說就是 無符號 。將符號位也用以儲存資料。
int i = 0xFF60; // 65376
System.out.println(i);
// 00000000000000001111111101100000
System.out.println(String.format("%32s", Integer.toBinaryString(i)).replace(" ", "0"));
byte b1 = (byte) i;
byte b2 = (byte) (i >> 8);
// 01100000
System.out.println(String.format("%8s", Integer.toBinaryString(b1 & 0xFF)).replace(" ", "0"));
// 11111111
System.out.println(String.format("%8s", Integer.toBinaryString(b2 & 0xFF)).replace(" ", "0"));
int ret = (b1 & 0xFF) | (b2 & 0xFF) << 8;
System.out.println(String.format("%32s", Integer.toBinaryString(ret)).replace(" ", "0"));
// 65376
System.out.println(ret);
若沒有做 & 0xFF 操作,其值應是:
/*
* 0000 0000 0000 0000 0000 0000 0110 0000 (b1)
* |
* 1111 1111 1111 1111 1111 1111 0000 0000 (b2<<8)
* =======================================
* 1111 1111 1111 1111 1111 1111 0110 0000 (-160)
*/
System.out.println(b1 | b2 << 8); // "-160"
65376 本質來說超過了short的儲存範圍:-32768~32767 ,但其在int中依然只需佔2個位元組16位:65376<65536。所以我們只需要使用2個byte即可儲存,而不需要int的4個byte來儲存。
在Socket傳輸中使用這樣的方式能有效降低傳輸的位元組冗餘。
案例-多Flag儲存在一個byte中
有這樣一個情形:一個四邊形,四條邊可以是虛線也可以是實線,四條邊相互獨立;定義為 a\b\c\d 四邊;此時我們需要在畫布上畫出這個四邊形;但是因為4邊相互獨立,所以我們常見的就是定義4個bool值:
boolean a = true;
boolean b = false;
boolean c = false;
boolean d = true;
void changeA(boolean fullLine) {
a = fullLine;
}
簡單來說我們定義這樣的方式其一比較麻煩,其二總佔用的記憶體空間至少是4個byte,也有可能是16byte(按int存的情況)。
但是我們表示的內容無非就是2種:實線、虛線
所以我們可以這樣做:
static byte a = 0b00000001;
static byte b = 0b00000010;
static byte c = 0b00000100;
static byte d = 0b00001000;
byte x = 0b00000000;
定義a、b、c、d為static,並且使用最後的4位即可。
若我們想要改變a邊的實虛:
void changeA(boolean fullLine) {
if (fullLine) {
x = (byte) (x | a);
} else {
x = (byte) (x & ~a);
}
}
通過該方法,若a邊為實線,則將a flag的值填入x中,反之擦除掉x中的a邊資訊;同時保證其他資訊不變。
若要拿,也就是判斷a是否為實線該如何辦?
boolean isFullLine() {
// return (x & a) != 0;
return (x & a) == a;
}
2種寫法都是OK的,不過需要注意若對應的a使用了符號位則需要使用0xFF先清理自動補充的符號位。因為與、或、非等操作預設會將引數轉化為int型別進行;所以會出現自動補充符號位的情況。
這樣的操作方案在Android或Socket傳輸中都是非常常見的,比如Socket NIO中的SelectorKey中的ops變數就是這樣的機制;這能有效減少儲存多個引數的情況;並且位操作並不會帶來多少計算負擔。
了讓學習變得輕鬆、高效,今天給大家免費分享一套Java入門教學資源。幫助大家在成為Java架構師的道路上披荊斬棘。需要資料的歡迎加入學習交流群:9285,05736
