背景
很多项目中都会存储用户状态,诸如用户类型、相关操作权限等等。
比较常用的方案有两种。
- 使用一个int类型字段存储用户状态,不同的数字代表不同的状态
- 比较特殊的状态需要另外使用字段存储
方案一的问题在于无法存储复合状态。例如某个用户既是普通用户又处于封禁状态,想要使用方案一存储则数字会相当的多,不利于开发记忆。
因此我使用位运算方案来存储状态
思路
位运算的思路为利用一个int(或者long)的不同位来存储不同状态。
例如 状态数字 10,换算成二进制长这样:
1010
这样一看,一个数字 10 就有四个位,可以存储四个状态。
这个状态就可以这样解释:
最低位的0代表这是一个普通用户
第二低的1代表用户未曾登录
第二高的0代表用户可以使用手机号登录
最高位的1代表用户要接收消息通知
当然,每一位代表的意义在此并不重要。重要的是,每一个bit有两种状态:0和1,需要定义的是0代表enable还是1代表disable。这影响的是每一个bit的实际意义,也和接下来的方法定义有关。
同时,为了后续扩展,在进行状态定义的时候,最好要从低位往高位定义,高位未定义时必定为0。当用户需要增加新状态时,将新状态定义为1,那么原本的用户的状态数据可以不作修改。
存储效率:
以int类型为例,int占4个字节,共有4x8=32位,意为可以同时存储32种不同的状态,在实际意义上则代表2^32种现实意义
实现
定义
首先,
需要对要用到的位进行定义。
在此使用枚举来进行定义;
同时,
需要标明状态被定位在哪一个bit上
这一点很容易实现,使用2的幂指数即可;比如2=2^1,代表定位到从低往高数第二位的位置上
因此枚举定义如下:
public enum UserStatus {
//以下均为代表不可用状态
USERNAME(1),//是否可以使用username字段登录
MOBILE(2),//是否可以使用手机号登录
SMSCODE(4),//是否可以通过短信验证码登录
SIGN(8),//是否可签署合同
SEND(16),//是否发送合同
INIT_PASSWORD(32),//是否是初始密码,0代表是初始密码,一个用户刚注册或导入的情况下都是初始密码(默认密码)
INIT_SIGN_PASSWORD(64),//是否是默认的签署密码,0代表是默认密码
DIY_VERIFY(128),//是否是修改过的的身份认证信息
RECEIVE_SMS(256),//接收短信通知
RECEIVE_EMAIL(512),//接收邮件通知
RECEIVE_WEB_NOTICE(1024),//接收站内通知
;
int code;//需要指定的位数,应该都是2的幂指数,指定位上为1代表不允许
boolean userDiy;//是否允许普通用户自己修改指定状态
UserStatus(int code) {
this.code = code;
}
public int code() {
return code;
}
}
接下来需要对于状态的一些操作函数。我一共定义了三种:is、enable、disable;
is函数
首先是is函数:
方法申明如下:
boolean is(int code, BitInfo status);
其中:code代表存储了复合状态的状态数据,status代表某种定义的状态,我再次定义了一个BitInfo接口用于扩展,简单的理解成一个2的幂指数即可。
那么is函数的意义为:在code中的status对应位上是否为1
我使用的是 对应位 为 1,实际上0或者1不影响方法定义,只会对使用上有影响
接下来就是位运算的部分了
可以把这个函数实现分成两部分:一、取出指定位的数据;二、判断数字
还是拿 10 举例,在此我需要判断其从低往高数第二位是否为0
那么code传递的应该是10,status代表的幂指数应该是2
10化为二进制如下
1010
2化为二进制如下
0010
很明显可以看出,只需要将两个数字进行与(&)运算,即可以拿出指定位;准确的说是将无关位置为0,指定位不变。
结果是:
0010
再与2做个比对即可。
因此方法实现如下:
boolean is(int code, BitInfo status) {
//和指定位进行与运算后,对应的结果为0,即为允许,status.code 为1,代表不允许
return (code & status.code()) != status.code();
}
disable
disable函数的意义是:将指定位置为1。
要求:
- 随便一个状态数字在disable之后进行is返回false
- 对状态数字进行反复disable,结果都不能有变化(意思是不能简单的使用取反操作)
方法申明如下
int disable(int code, BitInfo status);
在此将10的最低位置为1
相关参数为
- code:10
- status:1
1的二进制如下
0001
很明显可以看出,只需要将两个数字进行 或(|)运算即可
实现如下
int disable(int code, int status) {
//将指定位数置为1 即不允许
return code | status;
}
enable
enable函数的意义是:将指定位置为0。
要求:
- 随便一个状态数字在enable之后进行is返回true
- 对状态数字进行反复enable,结果都不能有变化(意思是不能简单的使用取反操作)
方法申明如下
int enable(int code, BitInfo status);
在此将10的最低位置为1
相关参数为
- code:10
- status:1
在disable函数中,已经将指定位置为1了,那么只需要把这个1再给变回来即可,且不能影响其他位
因此只需要将10和2先后进行或运算和异或运算即可
方法实现如下:
int enable(int code, BitInfo status) {
//将指定位数置为0 即允许
//将位和1 或,则指定位一定为1,再与status异或,指定位则为0
return (code | status.code()) ^ status.code();
}
使用
那么在使用上也很简单。
例如我要判断 状态 254 是否可以接收短信,只需要这样调用
BitInfo.is(254,UserStatus.RECEIVE_SMS)
返回true即代表可以接收短信
总结
使用位运算存储状态有以下优点:
- 节约空间
- 易于扩展
- 可以同时存储复合状态
但是缺点也很明显:
- 不够直观,必须通过程序才能看出具体状态含义
- 逻辑复杂,过于专注底层位运算可能会搞混,但是只关注抽象层就好得多
上述代码中还是有一定问题的。例如同时enable多个状态,调用上就比较复杂,会出现很多括号;这可以通过链式调用来解决,我在此就不作修改了。
来源:oschina
链接:https://my.oschina.net/inkbox/blog/3190351