巧用位运算能极大的精简代码和提高程序效率。所以,在一些优秀的开源代码中,经常能出现位运算。所以,把位运算这种思想迁移到业务代码里,有时候往往能起到柳暗花明般的重构。
位运算
在 JAVA 语言中,定义了诸多的位运算符,如下所示:
运算符 | 描述 |
---|---|
& | 与 |
| | 或 |
~ | 非 |
^ | 异或 |
<< | 左移 |
>> | 右移 |
&(与)
十进制 | 二进制 |
---|---|
3 | 0 0 1 1 |
5 | 0 1 0 1 |
& 后结果:1 | 0 0 0 1 |
即:对应位都为 1 时,才为 1,否则全为 0。
|(或)
十进制 | 二进制 |
---|---|
3 | 0 0 1 1 |
5 | 0 1 0 1 |
| 后结果 :7 | 0 1 1 1 |
即:对应位只要有 1 时,即为 1,否则全为 0。
~(非)
十进制 | 二进制 |
---|---|
3 | 0 0 1 1 |
~ 后结果:12 | 1 1 0 0 |
即:对应位取反。
异或 ^
十进制 | 二进制 |
---|---|
3 | 0 0 1 1 |
5 | 0 1 0 1 |
^ 后结果:6 | 0 1 1 0 |
即:只要对应为不同即为 1。
使用位运算重构项目
当前我们需要设计一个权限模块,可动态的为用户指定某个文件的操作权限。并且,用户对一个文件的操作权限分为:读(R),写(W),执行(X)。
这是一个很简单的需求,为了描述这种关系,我们会在数据库表关系设计时,定义如下的结构:
数据表:user_file_permission
字段 | 类型 | 备注 |
---|---|---|
userId | int | 用户 |
fileId | int | 文件 |
readable | bit | 是否可读 |
writable | bit | 是否可写 |
executable | bit | 是否可执行 |
映射的模型:UserFilePermission
public class UserFilePermission {
/**
* 用户
*/
private User user;
/**
* 文件
*/
private File file;
/**
* 读操作
*/
private Boolean readable;
/**
* 写操作
*/
private Boolean writable;
/**
* 执行操作
*/
private Boolean executable;
}
这是常见的实现方式。但考虑下,业务需求千变万化,倘若需要再新增一个下载(D) 操作,是不是需要去额外扩展一个字段。所以,对于长期来讲,有值得重构的空间。
故缺点很明显:
- 难扩展
- 繁琐,比如判断是否包含读和执行的操作权限,需要这样写
if(xx.IsReadable() && xx.IsExecutable())
,但随着权限操作越来越多时,if
代码块也越来越大。
位运算重构
了解 Linux
的同学一定知道利用 chmod
来控制文件如何被他人调用。比如针对一个文件,可分别给 User、Group、Other 设置访问的权限。同时权限操作分为:r(读),w(写),x(执行)。很巧,和我们的需求一样。那我们来看下Linux
是如何实现权限控制的。
核心是定义一个整数来代表操作权限,即:r=4,w=2,x=1
- 若要 rwx 权限,则:4+2+1=7;
- 若要 rw- 权限,则:4+2=6;
- 若要 r-x 权限,则:4+1=5。
所以使用 chmod 也可以用数字来表示权限,如下即给 User、Group、Other 三个维度的对象都设置了代表可读、可写、可执行的权限,代号:7。
chmod 777 file
你可能会想,为什么 r=4,w=2,x=1?聪明的你,肯定想到了——二进制。
权限操作 | 二进制 | 十进制 |
---|---|---|
r | 0100 | 4 |
w | 0010 | 2 |
x | 0001 | 1 |
所以借由这个思想,我们对代码进行重构,去掉了readable
,writable
,executable
这三个字段,而统一由一个 permissoin
字段来表示,如下所示:
public class UserFilePermission {
/**
* 可执行(x):0001
*/
public static final int OP_EXECUTABLE = 1;
/**
* 可写(w):左移一位:0010
*/
public static final int OP_WRITABLE = 1 << 1;
/**
* 可读(r):左移二位:0100
*/
public static final int OP_READABLE = 1 << 2;
/**
* 用户
*/
private User user;
/**
* 文件
*/
private File file;
/**
* 权限
*/
private int permission;
}
其中 permission
的可选项如下表格所示:
permission | r | w | x | 描述 |
---|---|---|---|---|
1(0001) | 0 | 0 | 1 | 可执行 |
2(0010) | 0 | 1 | 0 | 可写 |
4(0100) | 1 | 0 | 0 | 可读 |
3(0011) | 0 | 1 | 1 | 可写、可执行 |
7(0111) | 1 | 1 | 1 | 可读、可写、可执行 |
0(0000) | 0 | 0 | 0 | 禁止 |
同时,操作权限不是一尘不变的,我们往往需要对其新增、删除、查询。通过位运算,可以非常方便实现。
为当前权限新增一个操作:
public void addOp(int op) {
permission |= op;
}
为当前权限删除一个操作:
public void removeOp(int op) {
permission &= ~op;
}
判断当前权限是否包含指定的操作权限:
public boolean containsOp(int op) {
return (permission & op) == op;
}
判断当前权限是否不包含指定的操作权限:
public boolean notContainsOp(int op) {
return (permission & op) == 0;
}
当然,这样的重构唯一的缺点就是可读性变差。当然,如果团队对位运算达成共识之后,大家都有一定的了解。相反,可读性还是可以的。同时,位运算的计算非常快,也在一定程度上提升了执行效率。