1. 程式人生 > >可能是最通俗易懂的 Java 位操作運算講解

可能是最通俗易懂的 Java 位操作運算講解

Java 位操作這是一項很基礎很基礎的知識內容,在所有 Android 和 Java 開發者的學習之路上,大家都接觸過,但是實際運用的場景卻很少見,很多人估計都忘記有這個知識點了。事實上,在 C/C++ 開發領域因為與硬體的聯絡更緊密,所以位操作運算應用的更普遍。Java 因為面向物件的特性很多時候不需要接觸位操作,但是在某些特定場景下,巧妙運用位操作,能夠起到非常高效的的表現。這篇博文不談應用,只詳細講解與位操作有關的知識點。

基礎大講堂

所有數值都是2進位制

軟體開發者都知道 10 進位制、16 進位制、8 進位制。
比如數字 10 的各位進位制形式表現如下。

十進位制:10
八進位制:012
十六進位制:0x0a
二進位制:1010

我們可以開啟系統的自帶的計算器(Win鍵 + R –> 輸入 CMD 開啟命令列視窗 –> calc 按回車),看看上面的結論。
這裡寫圖片描述

雖然有很多種進位制,但是實際上計算機所認識的資料只有 0 和 1,因此所有的數值不管它是十進位制、十六進位制也好都會統統在底層被翻譯成二進位制數值。

int a = 5;
//0101 就是 a 的二進位制表示。  

int b = 520;
//1000001000 就是 b 的二進位制表示

bit、byte、world

  1. bit (位) bit 電腦記憶體中最小的單位,在二進位電腦系統中,每一 bit 可以代表 0 或 1 的數位訊號。所以它能表示的數字範圍就是 0 ~ 1。
  2. byte (位元組) 一個 byte 由 8 bit 組成,所以理論上一個 byte 能表示的資料範圍是 0 ~ 255。
  3. word (字) 一個 word 由 2 byte 組成,所以理論上一個 word 能表示的資料範圍是 0 ~ 65535。

大家可以看這張圖加深下理解。
這裡寫圖片描述

32 位與 64 位作業系統。

一般計算機裝置上,CPU 主要有 32 位和 64 位(當然,微控制器有 8 位和 16 位),32 位 CPU 能夠定址的範圍是 4 GB。所以過去的電腦裝置記憶體最高一般只能到達 4 GB。後來,隨著晶片技術的發展,越來越多的機器採用了 64 位 CPU。這使得機器的最大記憶體可以為 16 GB。

那麼好,我們再來談談 32 位作業系統與 64 位作業系統。實際上它們分別是針對 CPU 型別設計的軟體系統。

32 bit 是 4 byte。通常一條 CPU 指令是 4 byte。在 32 位作業系統上,如果一條 CPU 指令是 4 byte,那麼 CPU 執行一次能夠讀取 32 bit 內容,所以一個指令週期內就能夠完成指令,如果一條 CPU 指令是 8 byte 的話,那麼 32 位作業系統就需要通過 2 個指令週期才能完成指令的讀取,而對應的 64 位作業系統因為一次能夠讀取 64 bit 內容,所以它在一個指令週期就能夠讀取指令。所以,理論上,64 位的作業系統是要比 32 位作業系統要快 1 倍。

但還有幾個需要大家注意的地方是:
1. 64 位 CPU 機器可以安裝 32 位作業系統,但效率自然跟 32 位作業系統一樣。
2. 32 位 CPU 機器也可以安裝 64 位作業系統。
3. 64 位 CPU 機器安裝 64 位作業系統才最有效率,但跟軟體優化也有關係。

不同的作業系統平臺,給 C/C++ 基本資料型別變數分配的位元組是不一樣的。

32位編譯器:

  char :1個位元組
  char*(即指標變數): 4個位元組(32位的定址空間是2^32, 即32個bit,也就是4個位元組。同理64位編譯器)
  short int : 2個位元組
  int:  4個位元組
  unsigned int : 4個位元組
  float:  4個位元組
  double:   8個位元組
  long:   4個位元組
  long long:  8個位元組
  unsigned long:  4個位元組

64位編譯器:

  char :1個位元組
  char*(即指標變數): 8個位元組
  short int : 2個位元組
  int:  4個位元組
  unsigned int : 4個位元組
  float:  4個位元組
  double:   8個位元組
  long:   8個位元組
  long long:  8個位元組
  unsigned long:  8個位元組

上面講的是 C/C++ 在不同平臺上的位元組長度差別,但是對於 Java 而言,由於 Java 是跨平臺語言,所以 JVM 表現下的基礎資料位元組長度其實都是一致的。


int4 個位元組。

short2 個位元組。

long8 個位元組。

byte1 個位元組。

float4 個位元組。

double8 個位元組。

char2 個位元組。

booleanboolean屬於布林型別,在儲存的時候不使用位元組,僅僅使用 1 位來儲存,範圍僅僅為01,其字面量為truefalse

我們可以看到 Java 與 C/C++ 的基本資料型別位元組長度有些不一致,所以涉及到網路通訊互動或者是 JNI 開發時,資料的轉換有時需要考慮下基礎的位元組長度。

本篇文章的主要內容是 Java 中的位操作,所以基礎資料長度也是以 Java 中定義的為準。

原碼 反碼 補碼

我們已經知道了一個 int 型數值是 4 個位元組。每個位元組有 8 位。但對於一個 int 或者其它整數型別如 (long)的數值而言還要注意的是,它的最高位是符號位。

  • 最高位為0表示正數。
  • 最高位為1表示負數

原碼 將一個數字轉換成二進位制就是這個數值的原碼。

int a = 5; //原碼  0000 0000 0000 0101
int b = -3;  //原碼  1000 0000 0000 0011

反碼
分兩種情況:正數和負數

  • 正數 正數的反碼就是原碼。
  • 負數 負數的反碼是在原碼的基礎上,符號位不變 其它位都取反。
5 的原碼:0000 0000 0000 0101

-3 的原碼:1000 0000 0000 0011
-3 的反碼:1111 1111 1111 1100

補碼
仍然分正數和負數兩種情況

  • 正數 正數的補碼就是原碼。
  • 負數 負數的補碼在反碼的基礎上加1。
5 的補碼:0000 0000 0000 0101


-3 的反碼:1111 1111 1111 1100
-3 的補碼: 1111 1111 1111 1101

計算機在進行數值運算的時候,是通過補碼錶示每個數值的。

比如

5 - 3 = 5 + ( -3 )
相當於 0000 0000 0000 0101 + 1111 1111 1111 1101
    = 1 0000 0000 0000 0010

最後的結果是1 0000 0000 0000 0010 這樣的二進位制,由於 int 型別只有 4 byte,所以最高位產生了溢位,進位 1 被丟棄。結果就變成了 0010 也就是 2,5 - 3 = 2 沒有毛病。

這裡寫圖片描述

位運算子 &、|、~、^、>>、<<

位運算子包含與運算子、或運算子、取反運算子、異或運算子、左移運算子和右移運算子。在下面的內容中,我將會一一講解。

需要注意的是,下面測試用的資料都是 int 型別,int 型別是 4 個位元組長度,但是為了方便說明示例中用的數值我都用 1 個位元組表示。希望不會給大家造成困擾。

& 與運算子

規則 與運算時,進行運算的兩個數,從最低位到最高位,一一對應。如果某 bit 的兩個數值對應的值都是 1,則結果值相應的 bit 就是 1,否則為 0.

0 & 0 = 0,

0 & 1 = 0,

1 & 1 = 1

3 & 5 = 1 這是因為

0000 0011

&

0000 0101

=
0000 0001

按照規則,將兩個數值按照低位到高位一一對齊運算,因為只有第 0 位都為 1,所以計算結果為 1.

| 或運算子

規則 與運算時,進行運算的兩個數,從最低位到最高位,一一對應。如果某 bit 的兩個數值對應的值只要 1 個為 1,則結果值相應的 bit 就是 1,否則為 0。

0 | 0 = 0,

0 | 1 = 1,

1 | 1 = 1

3 | 5 = 7 這是因為

0000 0011

|

0000 0101

=
0000 0111

~ 取反運算子

規則 對運算元的每一位進行操作,1 變成 0,0 變成 1。

~5 =>  0000 0101   ~  => 1111 1010

^ 異或運算子

規則 兩個運算元進行異或時,對於同一位上,如果數值相同則為 0,數值不同則為 1。

1 ^ 0 = 1,

1 ^ 1 = 0,

0 ^ 0 = 0;

3 ^ 5 = 6,這是因為

0000 0011

|

0000 0101

=
0000 0110

值得注意的是 3 ^ 5 = 6,而 6 ^ 5 = 3

0000 0110

|

0000 0101

=
0000 0011

針對這個特性,我們可以將異或運算作為一個簡單的資料加密的形式。比如,將一個mp4檔案所有數值與一個種子數值進行異或得到加密後的資料,解密的時候再將資料與種子數值進行異或一次就可以了。

所以說異或運算可以作為簡單的加解密運算演算法。

>> 右移運算子

規則 a >> b 將數值 a 的二進位制數值從 0 位算起到第 b - 1 位,整體向右方向移動 b 位,符號位不變,高位空出來的位補數值 0。

5 >> 1 ===>  1000 0000 0000 0101 >> 1  = 1000 0000 0000 0010 = 2
7 >> 2 ===>  1000 0000 0000 0111 >> 2  = 1000 0000 0000 0001 = 1
9 >> 3 ===>  1000 0000 0000 1001 >> 3  = 1000 0000 0000 0001 = 1
11 >> 2 ===> 1000 0000 0000 1011 >> 2 = 1000 0000 0000 0010 = 2

大家發現什麼規律沒有?a >> b = a / ( 2 ^ b ) ,所以 5 >> 1= 5 / 2 = 2,11 >> 2 = 11 / 4 = 2。

<< 左移運算子

規則 a << b 將數值 a 的二進位制數值從 0 位算起到第 b - 1 位,整體向左方向移動 b 位,符號位不變,低位空出來的位補數值 0。

5 << 1 ===>  1000 0000 0000 0101 << 1  = 1000 0000 0000 1010 = 10
7 << 2 ===>  1000 0000 0000 0111 << 2  = 1000 0000 0001 1100 = 28
9 << 3 ===>  1000 0000 0000 1001 << 3  = 1000 0000 0100 1000 = 72
11 << 2 ===> 1000 0000 0000 1011 << 2 = 1000 0000 0010 1100 = 44

很明顯就可以看出 a << b = a * (2 ^ b)

綜合上面兩個可以看到,如果某個數值右移 n 位,就相當於拿這個數值去除以 2 的 n 次冪。如果某個數值左移 n 位,就相當於這個數值乘以 2 ^ n。

總結

Java 的位運算內容就是上面講到的這些,這些東西都非常簡單。但是有時候簡單的東西卻能很大程式上提高開發效率。之所以想起寫這篇文章,是因為在閱讀 Android 相關程式碼時,正好看到了位運算的身影,只覺得非常好用和巧妙。下一篇文章我會專門來介紹 Java 位運算在 Android 原始碼中的巧妙應用。文章寫完後我會放上鍊接地址。

參考內容