1. 程式人生 > >Java位運算的基礎及使用(意義)

Java位運算的基礎及使用(意義)

前言

這幾天在看HashMap的原始碼,但裡面的位運算太多,看得有點暈。故,先整理位運算相關知識。
在瞭解位運算的計算後,又在思考,使用位運算的意義是什麼,畢竟平時開發基本沒用過位運算。經大量的資料查詢,整理了兩個自己感覺比較好的位運算利用例子,特在此記錄,分享。
另外,畢竟位運算的程式碼可讀性差,請大家謹慎使用。

一、位運算基礎

1、位運算是針對整數的二進位制進行的位移操作
2、整數 32位 , 正數符號為0,負數符號為1。十進位制轉二進位制 不足32位的,最高位補符號位,其餘補零
3、在Java中,整數的二進位制是以補碼的形式存在的
4、位運算計算完,還是補碼的形式,要轉成原碼,再得出十進位制值
5、正數:原碼=反碼=補碼 負數:反碼=原碼忽略符號位取反, 補碼=反碼+1

例如:十進位制4 轉二進位制在計算機中表示為(補碼) 00000000 00000000 00000000 00000100

例如:十進位制-4 轉二進位制在計算機中表示為(補碼) 11111111 11111111 11111111 11111100

負數轉二進位制過程(以-4為例)

原碼:10000000 00000000 00000000 00000100(轉二進位制,最高位為符號位)  
反碼:11111111 11111111 11111111 11111011(符號位不變,其餘取反)  
補碼:11111111 11111111 11111111 11111100(反碼+1)

-4 << 1 計算過程

-4 補碼  11111111 11111111 11111111 11111100  
左移一位 11111111 11111111 11111111 11111000 (這時候還是補碼)
# 如果最高位符號位為0,就不需要繼續操作了,因為正數的補碼=原碼,如果最高位是1,繼續往下走
轉成反碼 11111111 11111111 11111111 11110111 (補碼-1)  
轉成原碼 10000000 00000000 00000000 00001000 (忽略符號位取反)  
轉十進位制 -8  
  • 左移( << ) 整體左移,右邊空出位補零,左邊位捨棄 (-4 << 1 = -8)
  • 右移( >> ) 整體右移,左邊空出位補零或補1(負數補1,整數補0),右邊位捨棄 (-4 >> 1 = -2)
  • 無符號右移( >>> )同>>,但不管正數還是負數都左邊位都補0 (-4 >>> 1 = 2147483646)
  • 與( & )每一位進行比較,兩位都為1,結果為1,否則為0(-4 & 1 = 0)
  • 或( | )每一位進行比較,兩位有一位是1,結果就是1(-4 | 1 = -3)
  • 非( ~ ) 每一位進行比較,按位取反(符號位也要取反)(~ -4 = 3)
  • 異或( ^ )每一位進行比較,相同為0,不同為1(^ -4 = -3)

二、位運算應用

在一個系統中,使用者一般有查詢(Select)、新增(Insert)、修改(Update)、刪除(Delete)四種許可權,四種許可權有多種組合方式,也就是有16中不同的許可權狀態(2的4次方)。

Permission

一般情況下會想到用四個boolean型別變數來儲存:

public class Permission {

	// 是否允許查詢
	private boolean allowSelect;

	// 是否允許新增
	private boolean allowInsert;

	// 是否允許刪除
	private boolean allowDelete;

	// 是否允許更新
	private boolean allowUpdate;

	// 省略Getter和Setter
}

上面用四個boolean型別變數來儲存每種許可權狀態。

NewPermission

下面是另外一種方式,使用位掩碼的話,用一個二進位制數即可,每一位來表示一種許可權,0表示無許可權,1表示有許可權。

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;
	}
}

以上程式碼中,用四個常量表示了每個二進位制位程式碼的許可權項。例如:ALLOW_SELECT = 1 << 0 轉成二進位制就是0001,二進位制第一位表示Select許可權。ALLOW_INSERT = 1 << 1 轉成二進位制就是0010,二進位制第二位表示Insert許可權。private int flag儲存了各種許可權的啟用和停用狀態,相當於代替了Permission中的四個boolean型別的變數。用flag的四個二進位制位來表示四種許可權的狀態,每一位的0和1代表一項許可權的啟用和停用,下面列舉了部分狀態表示的許可權:

flag 刪除 修改 新增 查詢
1(0001) 0 0 0 1
2(0010) 0 0 1 0
4(0100) 0 1 0 0
8(1000) 1 0 0 0
3(0011) 0 0 1 1
0(0000) 0 0 0 0
15(1111) 1 1 1 1

使用位掩碼的方式,只需要用一個大於或等於0且小於16的整數即可表示所有的16種許可權的狀態。

此外,還有很多設定許可權和判斷許可權的方法,需要用到位運算,例如:

public void enable(int permission) {
	flag |= permission; // 相當於flag = flag | permission;
}

呼叫這個方法可以在現有的許可權基礎上新增一項或多項許可權。

新增一項Update許可權:

permission.enable(NewPermission.ALLOW_UPDATE);

假設現有許可權只有Select,也就是flag是0001。執行以上程式碼,flag = 0001 | 0100,也就是0101,便擁有了Select和Update兩項許可權。

新增Insert、Update、Delete三項許可權:

permission.enable(NewPermission.ALLOW_INSERT 
    | NewPermission.ALLOW_UPDATE | NewPermission.ALLOW_DELETE);

NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE | NewPermission.ALLOW_DELETE運算結果是1110。假設現有許可權只有Select,也就是flag是0001。flag = 0001 | 1110,也就是1111,便擁有了這四項許可權,相當於添加了三項許可權。上面的設定如果使用最初的Permission類的話,就需要下面三行程式碼:

permission.setAllowInsert(true);
permission.setAllowUpdate(true);
permission.setAllowDelete(true);

二者對比
設定僅允許Select和Insert許可權

Permission

permission.setAllowSelect(true);
permission.setAllowInsert(true);
permission.setAllowUpdate(false);
permission.setAllowDelete(false);

NewPermission

permission.setPermission(NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT);

判斷是否允許Select和Insert、Update許可權

Permission

if (permission.isAllowSelect() && permission.isAllowInsert() && permission.isAllowUpdate())

NewPermission

if (permission. isAllow (NewPermission.ALLOW_SELECT 
    | NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE))  

判斷是隻否允許Select和Insert許可權

Permission

if (permission.isAllowSelect() && permission.isAllowInsert() 
    && !permission.isAllowUpdate() && !permission.isAllowDelete())  

NewPermission

if (permission. isOnlyAllow (NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT))  

二者對比可以感受到MyPermission位掩碼方式相對於Permission的優勢,可以節省很多程式碼量,位運算是底層運算,效率也非常高,而且理解起來也很簡單。

.

可以替代位域的更好的方案
在《Effective Java》一書中,更推薦用EnumSet來代替位域:位域表示法也允許利用位操作,有效的執行像union和intersection這樣的集合操作。但位域有著int列舉常量所有的缺點,甚至更多。當位域以數字形式列印時,翻譯位域比翻譯簡單的int列舉常量要困難很多。甚至要遍歷位域表示的所有元素也沒有很容易的方法。

public class Text {
    public static final int STYLE_BOLD          = 1 << 0;
    public static final int STYLE_ITALIC        = 1 << 1;
    public static final int STYLE_UNDERLINE     = 1 << 2;
    public static final int STYLE_STRIKETHROUGH = 1 << 3;

    public void applyStyles(int styles) {...}
}

呼叫:

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);  

改成EnumSet的寫法是:

public class Text {

    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }

    public void applyStyles(Set<Style> styles) {
        System.out.println(styles);
    }
}  

呼叫:

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

三、位運算試題

有 1000 個一模一樣的瓶子,其中有 999 瓶是普通的水,有一瓶是毒藥。任何喝下毒藥的生物都會在一星期之後死亡。現在,你只有 10 只小白鼠和一星期的時間,如何檢驗出哪個瓶子裡有毒藥?

根據2^10=1024,所以10個老鼠可以確定1000個瓶子具體哪個瓶子有毒。具體實現跟3個老鼠確定8個瓶子原理一樣。
000=0
001=1
010=2
011=3
100=4
101=5
110=6
111=7
一位表示一個老鼠,從左到右分別代表老鼠3,老鼠2,老鼠1。0-7表示8個瓶子。也就是分別將1、3、5、7號瓶子的藥混起來給老鼠1吃,2、3、6、7號瓶子的藥混起來給老鼠2吃,4、5、6、7號瓶子的藥混起來給老鼠3吃,哪個老鼠死了,相應的位標為1。如老鼠1死了、老鼠2沒死、老鼠3死了,那麼就是101=5號瓶子有毒。同樣道理10個老鼠可以確定1000個瓶子

感謝