1. 程式人生 > >java位運算基本原理與實際運用

java位運算基本原理與實際運用

在讀jdk原始碼時,經常會遇到形如這樣的程式碼:

public static int numberOfLeadingZeros(int i) {
        // HD, Figure 5-6
        if (i == 0)
            return 32;
        int n = 1;
        if (i >>> 16 == 0) { n += 16; i <<= 16; }
        if (i >>> 24 == 0) { n +=  8; i <<=  8; }
        if (i >>> 28 == 0) { n +=  4; i <<=  4; }
        if (i >>> 30 == 0) { n +=  2; i <<=  2; }
        n -= i >>> 31;
        return n;
    }
或者是這樣:
if ((s & (s-1)) != 0)

       由於上學時候對位操作認識不深,工作後運用也偏少,逐也漸漸淡忘,以致看到這般程式碼只能忽略而過,今天除錯一個使用fork/join模型開發的工具時,再次遇到位運算,於是就上網找了很多資料,將位運算的基本原理和常見的應用場景整理下來。

一、關於二進位制原碼反碼補碼的基礎知識

       介紹位操作之前,先介紹原碼、反碼、補碼相關知識。我們都知道,計算機底層運算都是以二進位制的方式實現的,而我們平時生活計數都是用十進位制,相應我們在使用高階語言程式設計中也都是使用十進位制,那麼我們理所當然認為,編譯器把十進位制轉位二進位制計算不就可以了嗎,我們也這樣假設。

        我們都知道,java中int佔4個字元,佔4*8=32個位元組,由於整數有正負則第一個位元組存符號位,那麼最大值就是2的31次方-1,那麼二進位制相加很簡單:

1+1=2  二進位制為: 0001(1)+0001(1)=0010(2)  (為了方便閱讀,我們只用4個位元組,相當於byte)

那麼問題來了,因為計算機只有加法操作,沒有減法操作,所以減法操作是要轉化為加法操作的,那麼我們再看下面的程式碼:

1-1=1+(-1)=0  二進位制:0001(1)-0001(1)=0001(1)+1001(-1)=1010(-2) 很顯然是不對的。那麼就需要制定一種規則,對需要參與運算的變數應用這種規則,可以實現對減法轉為加法計算,結果為我們期望的值:假設規則為fit(),則需要滿足如下條件:

fit(1)+fit(1)=fit(2)、fit(1)-fit(1)=fit(1)+fit(-1)=fit(0) 轉為二進位制則為:fit(0001)+fit(0001)=fit(0010)、fit(0001)-fit(0001)=fit(0001)+fit(1001)=fit(0000) 

經過這幫人的琢磨,於是他們定了這麼一個規則:【計算補碼:負數的補碼就是對反碼加一,而正數不變,正數的原碼反碼補碼是一樣的。】

在補碼中用(-128)代替了(-0),所以補碼的表示範圍為:(-128~0~127)共256個。

補碼的計算很簡單,在反碼的基礎上加一,比如1001求反碼為1110(符號位不變,其他都取反),加一得1111。如此1-1的運算變為這樣:

0001-10001=0001(正數補碼與原碼一樣)+1111(負數補碼)= 0000這就對了。

其實我們不難發現,負數的原碼即為:正數的原碼取反,這就是這就是補碼的作用,這麼一來,負數在計算機中的表示法就成了補碼了。

二、位移運算

(1) <<     左移   
(2) >>     右移
(3) >>>    無符號右移

注意:位移操作只針對整型資料有效,包括int,byte,short,char,long,處int外,其他四種類型JVM會先把它們轉換成int型再進行操作。

m<<n的含義:把整數m表示的二進位制數左移n位,高位移出n位都捨棄,低位補0.  (此時將會出現正數變成負數的形式)
例項(為了便於閱讀,我們只用8個位元組):
 1)、 m<<n的含義:把整數m表示的二進位制數左移n位,高位移出n位都捨棄,低位補0.  (此時將會出現正數變成負數的形式)

3<<2剖析:
  3 的二進位制位00000011,左移兩位後得到00001100,即為12. (12=3*2^2)
  注意:左移可能使整數變為負數,即如果左移後最高位為1

2)、m>>n 的含義:把整數m表示的二進位制數右移n位,m為正數,高位全部補0;m為負數,高位全部補1.
例項: 
  3>>2剖析:
  3二進位制形式: 00000011,右移2位得到00000000,即為0.
  -3>>2剖析:
  -3二進位制形式: 11111101,右移2位,得到11111111,即為-1.(要注意了,這裡看到的可是補碼哈。)

以上:每個整數表示的二進位制都是32位的,如果右移32位和右移0位的效果是一樣的。依次類推,右移32的倍數位都一樣。

3)、m>>>n:整數m表示的二進位制右移n位,不論正負數,高位都補零。
例項: 
  3>>>2剖析:
  3二進位制形式: 00000011,高位補零右移2位,得到00000000,即為0.
  -3>>>2剖析:
  -3二進位制形式: 11111101,右移,得到00111111

【注】:對於以上三種操作,如果n為負數:這時JVM會先讓n對32取模,變成一個絕對值小於32的負數,然後再加上32,直到 n 變成一個正數。

三、按位操作

java中的位操作,包括四個操作符: (1)~(按位非) (2)|(按位或) (3)&(按位與) (4)^(按位異或) 編碼中對十進位制整型變數進行按位操作後,先對整型的二進位制形式進行操作,然後再轉換為整數,具體操作如下。

1).~(按位非):對該整數的二進位制形式逐位取反。
    ~4:(一元操作符)
     4的二進位制形式為:00000100,逐位取反後得到:11111011,即為-5.
2).|(按位或):對兩個整數的二進位制形式逐位進行邏輯或運算,原理為:1|0=1,0|0=0,1|1=1,0|1=1
等。
    4|-5:
     4的二進位制形式為:00000100,
    -5的二進位制形式為:11111011,
  逐位進行邏輯或運算:11111111,即得到-1.
3).&(按位與):對兩個整數的二進位制形式逐位進行邏輯與運算,原理:1|0=0,0|0=0,1&1=1;0&1=0等。  
   4&-5:
     4的二進位制形式為:00000100,
    -5的二進位制形式為:11111011,
  逐位進行邏輯與運算:00000000,即得到0.  

4).^(按位異或):【解義】對兩個整數的二進位制形式逐位進行邏輯異或運算,原理:1^1=0,1^0=1,0^1=1,0^0=0.
   4^-5:
     4的二進位制形式為:00000100,
    -5的二進位制形式為:11111011,
逐位進行邏輯異或運算:11111111,即得到-1.

四、實際運用

1、m<<n即在數字沒有溢位的前提下,對於正數和負數,左移n位都相當於m乘以2的n次方, m>>n即相當於m除以2的n次方,得到的為整數時,即為結果。如果結果為小數,此時會出現兩種情況:(1)如果m為正數,得到的商會無條件的捨棄小數位;(2)如果m為負數,捨棄小數部分,然後把整數部分加+1得到位移後的值。 比如取半數就可以用n>>1

2、按位異或可以比較兩個數字是否相等,它利用1^1=0,0^0=0的原理。  20^20==0

3、 判斷int型變數a是奇數還是偶數    
     a&1  = 0 偶數 
     a&1 =  1 奇數 
4、 求平均值,比如有兩個int型別變數x、y,首先要求x+y的和,再除以2,但是有可能x+y的結果會超過int的最大表示範圍,所以位運算就派上用場啦。
      (x&y)+((x^y)>>1); 
5、  對於一個大於0的整數,判斷它是不是2的幾次方
    ((x&(x-1))==0)&&(x!=0); 
6、 比如有兩個int型別變數x、y,要求兩者數字交換,位運算的實現方法:效能絕對高效
    x ^= y; 
    y ^= x; 
    x ^= y; 

7、 取模運算,採用位運算實現:
     a % (2^n) 等價於 a & (2^n - 1) 

8、 a % 2 等價於 a & 1 

9、在許可權的管理
public class NewPermission {
  // 是否允許查詢,二進位制第1位,0表示否,1表示是
  public static final int ALLOW_SELECT = 1 << 0; // 0001
  
  // 是否允許新增,二進位制第2位,0表示否,1表示是
  public static final int ALLOW_INSERT = 1 << 1; // 0010
  
  // 是否允許修改,二進位制第3位,0表示否,1表示是
  public static final int ALLOW_UPDATE = 1 << 2; // 0100
  
  // 是否允許刪除,二進位制第4位,0表示否,1表示是
  public static final int ALLOW_DELETE = 1 << 3; // 1000
  
  // 儲存目前的許可權狀態
  private int flag;

  /**
   *  重新設定許可權
   */
  public void setPermission(int permission) {
    flag = permission;
  }

  /**
   *  新增一項或多項許可權
   */
  public void enable(int permission) {
    flag |= permission;
  }
  
  /**
   *  刪除一項或多項許可權
   */
  public void disable(int permission) {
    flag &= ~permission;
  }
  
  /**
   *  是否擁某些許可權
   */
  public boolean isAllow(int permission) {
    return (flag & permission) == permission;
  }
  
  /**
   *  是否禁用了某些許可權
   */
  public boolean isNotAllow(int permission) {
    return (flag & permission) == 0;
  }
  
  /**
   *  是否僅僅擁有某些許可權
   */
  public boolean isOnlyAllow(int permission) {
    return flag == permission;
  }
}
詳情看http://www.tuicool.com/articles/Zr6R3q

10、在影象處理方面使用比較多,比如處理顏色時,ARGB資料是一個int,
int color 
可以用
((color & 0xFF000000)>>24) & 0xFF //得到A的值 ,也就是透明通道的值
((color & 0x00FF0000)>>16) & 0xFF //R 也就是紅色的值
((color & 0x0000FF00)>>8 ) & 0xFF //G 綠色的值
((color & 0x000000FF)    ) & 0xFF //B 藍色的值 
返過來也可以通過 A R G B的值來組成一個ARGB資料來進行處理。
當你要處理圖顏色時 你就發現,位移非常方便。

11、大小寫轉換:小寫轉大寫:(char)('a'&~32),大寫轉小寫:(char)('A'|32),當然我們一般還是用jdk的原生庫,這裡只做介紹。