最近在做一个类似百度知道类的系统,针对每个回答以及问题都有可能有多重组合状态:
- 是否过期
- 是否重复
- 是否优质
- ...
这些状态有个共同的特点,就是都是Yes Or Not,如果拍平,每个状态都用一个独立的字段进行存储,字段存储信息过于简单先不说,将来如果增加状态的话,数据库结构也需要同步修改。
正在研究怎么处理的时候,感谢同事的提点,给我了一个单一字段存储这些值的思路。经过实践后,发觉用的挺好,于是总结一下。
基本原理
首先我们已知这些状态都是Yes Or Not,且互斥的关系,不相交。因此我们可以将这些状态都看做是一个二进制数的一位,例如过期看做是0001,重复是0010,优质是0100,这样,如果数据库中字段值是3=(0b0011),就表示既过期,又重复。
这样如果我们将来状态不止这几种,只需要扩充二进制的位数即可。
计算
我们思路有了,如何提取结果就是目前的重要问题。
即:我给一个数据库值3=(0b0011),如何反推出是优质和过期。
这就涉及到三个按位操作符——&、|、^。
&(按位与)
逐位进行与运算
A | B | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
例如:
0011 & 0001 = 0001
0010 & 0001 = 0000
|(按位或)
逐位进行与运算
A | B | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
例如:
0011 | 0001 = 0011
0010 | 0001 = 0011
^(按位异或)
逐位进行与运算,相同为0,不相同为1
A | B | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
例如:
0011 ^ 0001 = 0010
0010 & 0001 = 0000
状态值计算
有个这三个工具后,就可以进行相应值的计算了
首先计算状态组合,用|运算,
例如:
一个问题是重复,不过期,不优质,就用0001 | 0000 | 0000 = 0001 = 1(int)
一个问题是重复,过期,不优质,就用0001 | 0010 | 0000 = 0011 =3(int)
每一种组合都对应唯一的一个二进制结果,这样将结果保存后,就一个用一个字段存储所有组合了。
然后我们有了这个计算结果后,还可以进行反推状态,这里用&操作
例如,刚才我们的一个计算结果是3,0b0011
我们首先用0011与“重复”对应的0001进行&操作
0011 & 0001 = 0001 ≠ 0,表示这个计算结果是包含“重复”状态的。
我们在用0011与“过期”对应的0010进行&操作
0011 & 0010 = 0010 ≠ 0,表示这个计算结果是包含“过期”状态的。
我们在用0011与“优质”对应的0100进行&操作
0011 & 0100 = 0000 = 0,表示这个计算结果是不包含“优质”状态的。
通过与相应二进制码进行&操作后结果是否等于0,可以判断状态。
除了进行反推,还需要个清除状态,可以去掉某个状态值。这一用到^操作
例如已知0011是包含重复状态,去除重复的话
0011 ^ 0001 = 0010 ,0010表示只过期状态。
一句话总结下:
添加状态用|,去掉状态用^(这里有个小坑,之后说),判断状态用&
具体代码
public enum Status {
DEFAULT(0b00000000, "默认", "default"),
REPEAT(0b00000001, "重复", "repeat"),
EXPIRE(0b00000010, "过期", "expire");
private int code;
private String desc;
private String property;
public static int getMark(Status... status) {
int init = 0;
for (AnswerStatus s : status) {
init = init | (s.getCode());
}
return init;
}
public static boolean status(int mark, Status status) {
return (mark & status.getCode()) == status.getCode();//判断是否相等也行,判断是否不等于0也可以
}
public static void handle(Consumer<Status> consumer) {
Arrays.stream(values()).forEach(consumer::accept);
}
}
生成状态值时,代码如下:
//保存mark
setMark(Status.getMark(
answer.isRepeat() ? Status.REPEAT : Status.DEFAULT,
answer.isExpire() ? Status.EXPIRE : Status.DEFAULT
))
//获取状态
repeat(AnswerStatus.status(answer.getMark(), AnswerStatus.REPEAT))
expire(AnswerStatus.status(answer.getMark(), AnswerStatus.EXPIRE))
这里生成状态值时,只能给出所有组合,进行保存,如果想根据当前状态与给定状态,进行组合更新,需要像下面这样调用:
例如目前,已经是0011了,我们给定expire=false,需要结果更新为0001
- 0011 & 0010 = 0010 ≠0,表示目前是过期,去掉过期通过处理,00110010 = 0001
- 0001 & 0010 = 0000 =0,表示原来就不是过期,所以不需要进行操作
这种单独修改,需要判断状态的转化,所以一般分为两步:
1、获取当前状态与目标状态
2、是-》是,否-》否,不需要操作;是到否,异或方式去掉状态;否到是,用或操作,将状态加上。
//mark:{repeat:true/false,expire:true/false}
Status.handle(element -> {
if (mark.containsKey(element.getProperty())) {
//需要进行调整
if ((pojo.getMark() & element.getCode()) != 0 && !mark.getBoolean(element.getProperty())) {
pojo.setMark(pojo.getMark() ^ (element.getCode()));
} else if ((pojo.getMark() & element.getCode()) == 0 && mark
.getBoolean(element.getProperty())) {
pojo.setMark(pojo.getMark() | (element.getCode()));
}
}
});
后续
这样就可以用一个字段进行存储了。其实我目前的方法不是最优解,实际上安卓系统在处理这种多状态时,已经在用这种二进制方式了,后续在研读安卓后,会尝试写一篇后续文章,优化目前的代码
网友评论