位运算(位掩码BitMask)的简单应用场景浅析

在Java中,位运算符有:与(&)、非(~)、或(|)、异或(^)、移位(<< 和 >>)、无符移位(<<< 和 >>>)。这些运算符在日常编码中运用并不多,但在看 Android 源码时发现其运用并不少,那么位运算究竟有什么利弊,合适的应用场景是什么呢?下面我们通过例子来进行探讨。

简单的例子

例如,在一个系统中,用户一般有查询(Select)、新增(Insert)、修改(Update)、删除(Delete)四种权限,四种权限有多种组合方式,也就是有16中不同的权限状态(2的4次方)。

一般情况下会想到用四个boolean类型变量来保存:

public class Permission {

    // 是否允许查询
    private boolean allowSelect;

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

    // 是否允许删除
    private boolean allowDelete;

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

    // 省略Getter和Setter
}

上面用四个boolean类型变量分别来保存每种权限状态。

通过位掩码实现

使用位掩码的话,用一个二进制数即可,每一位来表示一种权限,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 只允许查询(即等于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 四项权限都不允许
15(1111) 1 1 1 1 四项权限都允许

使用位掩码的方式,只需要用一个大于或等于0且小于16的整数即可表示所有的16种权限的状态。

此外,设置权限和判断权限的方法可以通过位运算实现,例如:

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

调用这个方法可以在现有的权限基础上添加一项或多项权限。

  1. 添加一项权限:

    permission.enable(NewPermission.ALLOW_UPDATE);
    

    假设现有权限只有Select,也就是flag是0001。执行以上代码,flag = 0001 | 0100,也就是0101,便拥有了Select和Update两项权限。

  2. 添加多项权限:

    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,便拥有了这四项权限,相当于添加了三项权限。

二者对比

  1. 设置仅允许Select和Insert权限

    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权限

    Permission:

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

    NewPermission:

    if (permission. isAllow (NewPermission.ALLOW_SELECT 
        | NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE))
    
  3. 判断是否只允许Select和Insert权限

    Permission:

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

    NewPermission:

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

二者对比可以感受到NewPermission位掩码方式相对于Permission的优势,可以节省很多代码量,位运算是底层运算,效率也非常高,而且理解起来也很简单。

小白鼠试药问题

我们先来看一道很常用被问到的面试题:

有 1000 个一模一样的瓶子,其中有 999 瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在,你只有 10 只小白鼠和一星期的时间,如何检验出哪个瓶子里有毒药?

根据2^9 < 1000 < 2^10=1024,所以10个老鼠可以确定1000个瓶子具体哪个瓶子有毒。具体实现跟3个老鼠确定8个瓶子原理一样。二进制表示如下:

二进制编码 十进制
000 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

三位的二进制编码位数中,每一位表示一只老鼠,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个瓶子(2^9 < 1000 < 2^10)

位运算符

1、左移运算符:<<

value << num : num 指定要移位值value 移动的位数。

左移的规则只记住一点:丢弃最高位,0补最低位

如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模

1 ) 运算规则
按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。
当左移的运算数是int 类型时,每移动1位它的第31位就要被移出并且丢弃;
当左移的运算数是long 类型时,每移动1位它的第63位就要被移出并且丢弃。
当左移的运算数是byte 和short类型时,将自动把这些类型扩大为 int 型。

2 )数学意义
在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方

3 )示例
先随便定义一个int类型的数int,十进制的value = 733183670,转换成二进制在计算机中的表示如下:

image

value << 1,左移1位


image

左移1位后换算成十进制的值为:1466367340,刚好是733183670的两倍, 有些人在乘2操作时喜欢用左移运算符来替代。

image

左移8位后变成了十进制的值为:-1283541504,移动8位后,由于首位变成了1,也就是说成了负数,在使用中要考虑变成负数的情况

根据这个规则,左移32位后,右边补上32个0值是不是就变成了十进制的0了?答案是NO,当int类型进行左移操作时,左移位数大于等于32位操作时,会先求余(%)后再进行左移操作。也就是说左移32位相当于不进行移位操作,左移40位相当于左移8位(40%32=8)。当long类型进行左移操作时,long类型在二进制中的体现是64位的,因此求余操作的基数也变成了64,也就是说左移64位相当于没有移位,左移72位相当于左移8位(72 % 64 = 8 )。

2、右移运算符:>>

value >> num:num 指定要移位值value 移动的位数。

右移的规则只记住一点:符号位不变,左边补上符号位

如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模

1)运算规则:
按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1(符号位扩展(保留符号位sign extension ))
当右移的运算数是byte 和short类型时,将自动把这些类型扩大为 int 型。

2 )数学意义
在数字没有溢出的前提下,右移一位相当于除2,右移n位相当于除以2的n次方。

3 )示例

还是这个数:733183670

image

value >> 1,右移1位
image

右移1位后换算成十进制的值为:366591835,刚好是733183670的1半, 有些人在除2操作时喜欢用右移运算符来替代。
value >> 8,右移8位看一下
image

和左移一样,int类型移位大于等于32位时,long类型大于等于64位时,会先做求余处理再位移处理,byte,short移位前会先转换为int类型(32位)再进行移位。以上是正数的位移,我们再来看看负数的右移运算,如图,负数intValue:-733183670的二进制表现如下图:
image

右移8位,intValue >> 8
image

3、无符号右移运算符:>>>

value >>> num:num 指定要移位值value 移动的位数。

无符号右移的规则只记住一点:忽略了符号位扩展,0补最高位

如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模

其他与右移运算符一样

1 )示例

以-733183670>>>8为例来画一下图


image

4、按位与&

只有两个操作数对应位同为1时,结果为1,其余全为0. (或者是只要有一个操作数为0,结果就为0)

5、按位或|

只有两个操作数对应位同为0时,结果为0,其余全为1.(或者是只要有一个操作数为1,结果就为1)

6、按位非~

对操作数每位进行取反

7、按位异或 ^

只有两个操作数对应位不同即为1

位运算实际运用

1、判断奇偶

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

2、数据交换

    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与十六进制数的转换

转换原理

Java中byte每个字符是由8个bit组成的,而16进制中每个字符由4个bit组成的[16进制中最大为:0xF(15)转为二进制为:1111组成的4个bit]。所以我们可以把一个byte转换成两个16进制字符,即把高4位和低4位转换成相应的16进制字符,并组合这两个16进制字符串,从而得到byte的16进制字符串。同理,相反的转换也是将两个16进制字符转换成一个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();
    }

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268