在开发中我们总是能遇到多选的情形,尤其是在配置中。
一般情况下我们可以使用一对多的存储关系或是append字符串的方式将所有选择项记录下来。但是一对多的存储关系占用资源多,append字符串的解析成本高,有没有更优的方式呢?答案肯定是有的,就是用位运算。
我们先将解决思路细化:
- 选择项其实就是选择与未选择,我们用
1和0来表示 - 不同的选择项我们用不同的标识来区分,比如序号
01234...
这样我们其实就很明确了,有序号,有01,那么我们直接用字节存储选项值就非常合适。
位存储
首先,我们要确定有多少个可选项,我们以Java举例,一个int是4个8位,那么逻辑上它就能存储4*8个选项值。
比如我们有一个星期配置,用来存储选择的星期数,那么我们可以指定第一位表示星期一,第二位表示星期二,依次类推。因此如果我们有这样一个数5,那么字节展开就是00000101,我们就知道我们选择了星期一和星期三。
位运算
有了存储逻辑,我们现在就需要把选项存入和取出了。
存入
还是拿星期配置来举例,我们可以计算出每个星期数的存储值:
| 星期数 | 存值 | 表示 |
|---|---|---|
| 星期一 | 1 | 1 << 0 |
| 星期二 | 2 | 1 << 1 |
| 星期三 | 4 | 1 << 2 |
| 星期四 | 8 | 1 << 3 |
| 星期五 | 16 | 1 << 4 |
| 星期六 | 32 | 1 << 5 |
| 星期日 | 64 | 1 << 6 |
那么我们是不是就可以在选择了星期一之后让星期配置+1,选择了星期三之后让星期配置+3呢?可以,但是这样不好。
我们设想另一个场景,现在有人丢给你了一个星期配置,说这个是默认的配置,你只能在默认配置的基础上进行增加。这时如果我们还是简单地增加星期配置值的话,必然会有重复增加的情况,此时的星期配置值就会出错。当然我们可以通过默认配置的值来推算出已经选择的是有哪些星期数,然后对他们进行排除避免重复增加,但是很显然这样做并不方便。
那么我们能不能在已经有星期一的情况下,继续增加星期一而让星期配置保持不变呢?就像下面这样:
00000001
00000001
-----------
00000001
1和1做运算得1,0和0做运算得0?没错,与运算和或运算都可以得到这一结果,但是这两个我们都可以用吗?我们来试试下面一个例子,此时我们星期配置中存储了星期一和星期三,再增加星期一的话我们期望的结果还是星期一和星期三:
00000101
00000001
-----------
00000101
在这里我们增加了一个条件,0和1运算得1,那么结果就很明显了,只能是或运算。
在代码中,我们就可以这样来进行星期配置:
// 星期配置
private static int weeks;
public static void main(String[] args) throws Exception {
allowedWeek(1); // 增加星期一
System.out.println(weeks);
allowedWeek(4 | 8); // 增加星期三和星期四
System.out.println(weeks);
allowedWeek(1); // 增加星期三
System.out.println(weeks);
}
public static void allowedWeek(int week) {
weeks = weeks | week;
}
得出得结果就是:
1
5
13
我们现在能通过或运算增加星期数,那么该怎么删除星期数呢?按照之前的思路,我们可以构建出下面的运算式,星期配置中存在有星期一和星期三,我们在去除星期一后只留下了星期三:
00000101
00000001
-----------
00000100
这里看起来好像异或就可以解决,但是如果我们再给定条件:在星期配置中存在星期一和星期三的情况下去除星期一和星期天,此时我们期望得到的就应该只有星期三,但是如果是异或的话,结果就变成了:
00000101
01000001
-----------
01000100
这里剩下了星期一与星期天,明显不是我们所期望的结果。
从运算过程可以看出,1和1运算得0,1和0运算得1或是0,0和0运算得0,这里我们是找不到运算符的。那么我们可以换个思路:去除星期一和星期天也就是其他星期数是允许的,那么也就是只要原有星期数与现在允许的星期数取交集就可以得到了期望结果:
000000101
110111110
------------
000000100
那么我们可以增加一个删除星期数的方法:
public static void blockedWeek(int week) {
weeks = weeks & ~week;
}
取出
很好,现在我们已经可以存储星期数配置了,那么我们怎么能从星期数配置中判断某个星期数是被选择的呢?实际上,或运算和与运算都可以实现选项匹配,匹配方式不同:
- 或运算中,
配置 | 判断项 = 配置时,表示判断项在配置中都存在。 - 与运算中,
配置 & 判断项 = 判断项时,表示判断项在配置中都存在。
写在代码中就像这样:
public static boolean weekOk(int week) {
return (weeks & week) == week;
// return (weeks | week) == weeks;
}
综述
最终我们通过不同的位运算就可以只使用一个int类型参数完成多选项的星期配置了:
allowedWeek(1); // 增加星期一
System.out.println(weeks); // output:1
System.out.println(weekOk(1)); // 是否存在星期一,output:true
System.out.println(weekOk(4)); // 是否存在星期三,output:false
allowedWeek(4); // 增加星期三
System.out.println(weeks); // output:5
System.out.println(weekOk(1)); // 是否存在星期一,output:true
System.out.println(weekOk(4)); // 是否存在星期三,output:true
allowedWeek(4 | 8); // 增加星期三和星期四
System.out.println(weeks); // output:13
System.out.println(weekOk(4)); // 是否存在星期三,output:true
System.out.println(weekOk(8)); // 是否存在星期四,output:true
blockedWeek(4); // 删除星期三
System.out.println(weeks); // output:9
System.out.println(weekOk(1)); // 是否存在星期一,output:true
System.out.println(weekOk(4)); // 是否存在星期三,output:false