位運算(位掩碼BitMask)的簡單應(yīng)用場景淺析

在Java中例嘱,位運算符有:與(&)狡逢、非(~)、或(|)拼卵、異或(^)奢浑、移位(<< 和 >>)、無符移位(<<< 和 >>>)腋腮。這些運算符在日常編碼中運用并不多雀彼,但在看 Android 源碼時發(fā)現(xiàn)其運用并不少,那么位運算究竟有什么利弊低葫,合適的應(yīng)用場景是什么呢详羡?下面我們通過例子來進(jìn)行探討。

簡單的例子

例如嘿悬,在一個系統(tǒng)中实柠,用戶一般有查詢(Select)、新增(Insert)善涨、修改(Update)窒盐、刪除(Delete)四種權(quán)限草则,四種權(quán)限有多種組合方式,也就是有16中不同的權(quán)限狀態(tài)(2的4次方)蟹漓。

一般情況下會想到用四個boolean類型變量來保存:

public class Permission {

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

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

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

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

    // 省略Getter和Setter
}

上面用四個boolean類型變量分別來保存每種權(quán)限狀態(tài)炕横。

通過位掩碼實現(xiàn)

使用位掩碼的話,用一個二進(jìn)制數(shù)即可葡粒,每一位來表示一種權(quán)限份殿,0表示無權(quán)限,1表示有權(quán)限嗽交。具體代碼如下:

public class NewPermission {

    // 是否允許查詢卿嘲,二進(jìn)制第1位,0表示否夫壁,1表示是
    public static final int ALLOW_SELECT = 1 << 0; // 0001

    // 是否允許新增拾枣,二進(jìn)制第2位,0表示否盒让,1表示是
    public static final int ALLOW_INSERT = 1 << 1; // 0010

    // 是否允許修改梅肤,二進(jìn)制第3位,0表示否邑茄,1表示是
    public static final int ALLOW_UPDATE = 1 << 2; // 0100

    // 是否允許刪除姨蝴,二進(jìn)制第4位,0表示否肺缕,1表示是
    public static final int ALLOW_DELETE = 1 << 3; // 1000

    // 存儲目前的權(quán)限狀態(tài)
    private int flag;

    /**
     * 重新設(shè)置權(quán)限
     */
    public void setPermission(int permission) {
        flag = permission;
    }

    /**
     * 添加一項或多項權(quán)限
     */
    public void enable(int permission) {
        flag |= permission;
    }

    /**
     * 移除一項或多項權(quán)限
     */
    public void disable(int permission) {
        flag &= ~permission;
    }

    /**
     * 是否擁有某項權(quán)限
     */
    public boolean isAllow(int permission) {
        return (flag & permission) == permission;
    }

    /**
     * 是否禁用了某些權(quán)限
     */
    public boolean isNotAllow(int permission) {
        return (flag & permission) == 0;
    }

    /**
     *  是否僅僅擁有某些權(quán)限
     */
    public boolean isOnlyAllow(int permission) {
        return flag == permission;
    }
}

以上代碼中似扔,用四個常量表示了每個二進(jìn)制位代碼的權(quán)限項。

ALLOW_SELECT = 1 << 0 轉(zhuǎn)成二進(jìn)制就是0001搓谆,二進(jìn)制第一位表示Select權(quán)限炒辉。
ALLOW_INSERT = 1 << 1 轉(zhuǎn)成二進(jìn)制就是0010,二進(jìn)制第二位表示Insert權(quán)限泉手。

private int flag存儲了各種權(quán)限的啟用和停用狀態(tài)黔寇,相當(dāng)于代替了Permission中的四個boolean類型的變量。

用flag的四個二進(jìn)制位來表示四種權(quán)限的狀態(tài)斩萌,每一位的0和1代表一項權(quán)限的啟用和停用缝裤,下面列舉了部分狀態(tài)表示的權(quán)限:

flag 刪除 修改 新增 查詢 權(quán)限
1(0001) 0 0 0 1 只允許查詢(即等于ALLOW_SELECT)
2(0010) 0 0 1 0 只允許新增(即等于ALLOW_INSERT)
4(0100) 0 1 0 0 只允許修改(即等于ALLOW_UPDATE)
8(1000) 1 0 0 0 只允許刪除(即等于ALLOW_DELETE)
3(0011) 0 0 1 1 只允許查詢和新增
0 0 0 0 0 四項權(quán)限都不允許
15(1111) 1 1 1 1 四項權(quán)限都允許

使用位掩碼的方式,只需要用一個大于或等于0且小于16的整數(shù)即可表示所有的16種權(quán)限的狀態(tài)颊郎。

此外憋飞,設(shè)置權(quán)限和判斷權(quán)限的方法可以通過位運算實現(xiàn),例如:

  public void enable(int permission) {
        flag |= permission;
    }

調(diào)用這個方法可以在現(xiàn)有的權(quán)限基礎(chǔ)上添加一項或多項權(quán)限姆吭。

  1. 添加一項權(quán)限:

    permission.enable(NewPermission.ALLOW_UPDATE);
    

    假設(shè)現(xiàn)有權(quán)限只有Select榛做,也就是flag是0001。執(zhí)行以上代碼,flag = 0001 | 0100检眯,也就是0101厘擂,便擁有了Select和Update兩項權(quán)限。

  2. 添加多項權(quán)限:

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

    NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE | NewPermission.ALLOW_DELETE運算結(jié)果是1110锰瘸。假設(shè)現(xiàn)有權(quán)限只有Select刽严,也就是flag是0001。flag = 0001 | 1110避凝,也就是1111舞萄,便擁有了這四項權(quán)限,相當(dāng)于添加了三項權(quán)限管削。

二者對比

  1. 設(shè)置僅允許Select和Insert權(quán)限

    Permission:

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

    NewPermission:

    permission.setPermission(NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT);
    
  2. 判斷是否允許Select鹏氧,Insert和Update權(quán)限

    Permission:

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

    NewPermission:

    if (permission. isAllow (NewPermission.ALLOW_SELECT 
        | NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE))
    
  3. 判斷是否只允許Select和Insert權(quán)限

    Permission:

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

    NewPermission:

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

二者對比可以感受到NewPermission位掩碼方式相對于Permission的優(yōu)勢,可以節(jié)省很多代碼量佩谣,位運算是底層運算,效率也非常高实蓬,而且理解起來也很簡單茸俭。

小白鼠試藥問題

我們先來看一道很常用被問到的面試題:

有 1000 個一模一樣的瓶子,其中有 999 瓶是普通的水安皱,有一瓶是毒藥调鬓。任何喝下毒藥的生物都會在一星期之后死亡。現(xiàn)在酌伊,你只有 10 只小白鼠和一星期的時間腾窝,如何檢驗出哪個瓶子里有毒藥?

根據(jù)2^9 < 1000 < 2^10=1024居砖,所以10個老鼠可以確定1000個瓶子具體哪個瓶子有毒虹脯。具體實現(xiàn)跟3個老鼠確定8個瓶子原理一樣。二進(jìn)制表示如下:

二進(jìn)制編碼 十進(jìn)制
000 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

三位的二進(jìn)制編碼位數(shù)中奏候,每一位表示一只老鼠循集,0 - 7 表示8個瓶子。

① 將1蔗草、3咒彤、5、7號瓶子的藥混起來給老鼠1吃

② 將2咒精、3镶柱、6、7號瓶子的藥混起來給老鼠2吃

③ 將4模叙、5歇拆、6、7號瓶子的藥混起來給老鼠3吃

哪個老鼠死了,相應(yīng)的位標(biāo)為1查吊。如老鼠1死了谐区、老鼠2沒死、老鼠3死了逻卖,那么就是101 = 5號瓶子有毒宋列。
同樣道理10個老鼠可以確定1000個瓶子(2^9 < 1000 < 2^10)

位運算符

1、左移運算符:<<

value << num : num 指定要移位值value 移動的位數(shù)评也。

左移的規(guī)則只記住一點:丟棄最高位炼杖,0補(bǔ)最低位

如果移動的位數(shù)超過了該類型的最大位數(shù),那么編譯器會對移動的位數(shù)取模

1 ) 運算規(guī)則
按二進(jìn)制形式把所有的數(shù)字向左移動對應(yīng)的位數(shù)盗迟,高位移出(舍棄)坤邪,低位的空位補(bǔ)零。
當(dāng)左移的運算數(shù)是int 類型時罚缕,每移動1位它的第31位就要被移出并且丟棄艇纺;
當(dāng)左移的運算數(shù)是long 類型時,每移動1位它的第63位就要被移出并且丟棄邮弹。
當(dāng)左移的運算數(shù)是byte 和short類型時黔衡,將自動把這些類型擴(kuò)大為 int 型。

2 )數(shù)學(xué)意義
在數(shù)字沒有溢出的前提下腌乡,對于正數(shù)和負(fù)數(shù)盟劫,左移一位都相當(dāng)于乘以2的1次方,左移n位就相當(dāng)于乘以2的n次方

3 )示例
先隨便定義一個int類型的數(shù)int与纽,十進(jìn)制的value = 733183670侣签,轉(zhuǎn)換成二進(jìn)制在計算機(jī)中的表示如下:

image

value << 1,左移1位


image

左移1位后換算成十進(jìn)制的值為:1466367340急迂,剛好是733183670的兩倍影所, 有些人在乘2操作時喜歡用左移運算符來替代。

image

左移8位后變成了十進(jìn)制的值為:-1283541504僚碎,移動8位后型檀,由于首位變成了1,也就是說成了負(fù)數(shù)听盖,在使用中要考慮變成負(fù)數(shù)的情況胀溺。

根據(jù)這個規(guī)則,左移32位后皆看,右邊補(bǔ)上32個0值是不是就變成了十進(jìn)制的0了仓坞?答案是NO,當(dāng)int類型進(jìn)行左移操作時腰吟,左移位數(shù)大于等于32位操作時无埃,會先求余(%)后再進(jìn)行左移操作徙瓶。也就是說左移32位相當(dāng)于不進(jìn)行移位操作,左移40位相當(dāng)于左移8位(40%32=8)嫉称。當(dāng)long類型進(jìn)行左移操作時侦镇,long類型在二進(jìn)制中的體現(xiàn)是64位的,因此求余操作的基數(shù)也變成了64织阅,也就是說左移64位相當(dāng)于沒有移位壳繁,左移72位相當(dāng)于左移8位(72 % 64 = 8 )。

2荔棉、右移運算符:>>

value >> num:num 指定要移位值value 移動的位數(shù)闹炉。

右移的規(guī)則只記住一點:符號位不變,左邊補(bǔ)上符號位

如果移動的位數(shù)超過了該類型的最大位數(shù)润樱,那么編譯器會對移動的位數(shù)取模

1)運算規(guī)則:
按二進(jìn)制形式把所有的數(shù)字向右移動對應(yīng)的位數(shù)渣触,低位移出(舍棄),高位的空位補(bǔ)符號位壹若,即正數(shù)補(bǔ)零嗅钻,負(fù)數(shù)補(bǔ)1(符號位擴(kuò)展(保留符號位sign extension ))
當(dāng)右移的運算數(shù)是byte 和short類型時,將自動把這些類型擴(kuò)大為 int 型店展。

2 )數(shù)學(xué)意義
在數(shù)字沒有溢出的前提下养篓,右移一位相當(dāng)于除2,右移n位相當(dāng)于除以2的n次方壁查。

3 )示例

還是這個數(shù):733183670

image

value >> 1,右移1位
image

右移1位后換算成十進(jìn)制的值為:366591835剔应,剛好是733183670的1半睡腿, 有些人在除2操作時喜歡用右移運算符來替代。
value >> 8峻贮,右移8位看一下
image

和左移一樣席怪,int類型移位大于等于32位時,long類型大于等于64位時纤控,會先做求余處理再位移處理挂捻,byte,short移位前會先轉(zhuǎn)換為int類型(32位)再進(jìn)行移位船万。以上是正數(shù)的位移刻撒,我們再來看看負(fù)數(shù)的右移運算,如圖耿导,負(fù)數(shù)intValue:-733183670的二進(jìn)制表現(xiàn)如下圖:
image

右移8位声怔,intValue >> 8
image

3、無符號右移運算符:>>>

value >>> num:num 指定要移位值value 移動的位數(shù)舱呻。

無符號右移的規(guī)則只記住一點:忽略了符號位擴(kuò)展醋火,0補(bǔ)最高位

如果移動的位數(shù)超過了該類型的最大位數(shù),那么編譯器會對移動的位數(shù)取模

其他與右移運算符一樣

1 )示例

以-733183670>>>8為例來畫一下圖


image

4、按位與&

只有兩個操作數(shù)對應(yīng)位同為1時芥驳,結(jié)果為1柿冲,其余全為0. (或者是只要有一個操作數(shù)為0,結(jié)果就為0)

5兆旬、按位或|

只有兩個操作數(shù)對應(yīng)位同為0時假抄,結(jié)果為0,其余全為1.(或者是只要有一個操作數(shù)為1爵憎,結(jié)果就為1)

6慨亲、按位非~

對操作數(shù)每位進(jìn)行取反

7、按位異或 ^

只有兩個操作數(shù)對應(yīng)位不同即為1

位運算實際運用

1宝鼓、判斷奇偶

    public static boolean isOdd(int a) {
        return (a & 1) != 0;
    }

2刑棵、數(shù)據(jù)交換

    public static void swap(int a, int b){
        a^=b;
        b^=a;
        a^=b;
    }

3、求絕對值

    public static int abs(int x) {
        int y;
        y = x >> 31;
        return (x ^ y) - y;        //or: (x+y)^y
    }

4愚铡、byte與十六進(jìn)制數(shù)的轉(zhuǎn)換

轉(zhuǎn)換原理

Java中byte每個字符是由8個bit組成的蛉签,而16進(jìn)制中每個字符由4個bit組成的[16進(jìn)制中最大為:0xF(15)轉(zhuǎn)為二進(jìn)制為:1111組成的4個bit]。所以我們可以把一個byte轉(zhuǎn)換成兩個16進(jìn)制字符沥寥,即把高4位和低4位轉(zhuǎn)換成相應(yīng)的16進(jìn)制字符碍舍,并組合這兩個16進(jìn)制字符串,從而得到byte的16進(jìn)制字符串邑雅。同理片橡,相反的轉(zhuǎn)換也是將兩個16進(jìn)制字符轉(zhuǎn)換成一個byte。

    public static String byteArrayToHexString(byte[] data) {
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte b : data) {
            int v = b & 0xff;
            if (v < 16) {
                sb.append('0');
            }
            sb.append(Integer.toHexString(v));
        }
        return sb.toString();
    }

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淮野,一起剝皮案震驚了整個濱河市捧书,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骤星,老刑警劉巖经瓷,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洞难,居然都是意外死亡舆吮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門队贱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來色冀,“玉大人,你說我怎么就攤上這事柱嫌∧派。” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵慎式,是天一觀的道長伶氢。 經(jīng)常有香客問我趟径,道長,這世上最難降的妖魔是什么癣防? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任蜗巧,我火速辦了婚禮,結(jié)果婚禮上蕾盯,老公的妹妹穿的比我還像新娘幕屹。我一直安慰自己,他們只是感情好级遭,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布望拖。 她就那樣靜靜地躺著,像睡著了一般挫鸽。 火紅的嫁衣襯著肌膚如雪说敏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天丢郊,我揣著相機(jī)與錄音盔沫,去河邊找鬼。 笑死枫匾,一個胖子當(dāng)著我的面吹牛架诞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播干茉,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼谴忧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了角虫?” 一聲冷哼從身側(cè)響起沾谓,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎上遥,沒想到半個月后搏屑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體争涌,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡粉楚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亮垫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片模软。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饮潦,靈堂內(nèi)的尸體忽然破棺而出燃异,到底是詐尸還是另有隱情,我是刑警寧澤继蜡,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布回俐,位于F島的核電站逛腿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仅颇。R本人自食惡果不足惜单默,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忘瓦。 院中可真熱鬧搁廓,春花似錦、人聲如沸耕皮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凌停。三九已至粱年,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苦锨,已是汗流浹背逼泣。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留舟舒,地道東北人拉庶。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像秃励,于是被迫代替她去往敵國和親氏仗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345