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個瓶子
感謝