网站首页 文章专栏 高性能的位运算,以及在实际项目中的运用
比如就拿我自己博客网站来说,我的每一篇文章都有,是否顶置,是否公开,是否原创,是否热文,是否推荐等属性,那么对应的我就要在数据库中新建5个字端,分别用0和1表示是和否,其实这是很浪费的,每个字段就两个状态,以后如果还要增加字段的话,那么还要加数据库字段。
那么可能你们还见过用一个字段,0表示xxx,1表示xxx,2表示xxx,3表示xxx等,用这种方式可以吗,有的场景可以,有的就不太行,以博客文章为例,文章可能同时有多种属性,既是顶置文章,又是原创文章,这种方案就不行了,因为不能同时表示多种状态。
今天讲的位运算,就是可以解决这个问题,用一个字段就能同时表示多种状态。
左移,右移:
左移(<<): 符号位不变,低位补0。如:2<<2结果为8。
右移(>>): 低位溢出,符号位不变,并用符号位补溢出的高位。如:-6>>2结果为-2。
按位与(&)
按位与的运算规则
操作数1 | 0 | 0 | 1 | 1 |
操作数2 | 0 | 1 | 0 | 1 |
按位与 | 0 | 0 | 0 | 1 |
规则总结:只有两个操作数对应位同为1时,结果为1,其余全为0. (或者是只要有一个操作数为0,结果就为0)
按位或( | )
按位或运算规则:
操作数1 | 0 | 0 | 1 | 1 |
操作数2 | 0 | 1 | 0 | 1 |
按位或 | 0 | 1 | 1 | 1 |
规则总结:只有两个操作数对应位同为0时,结果为0,其余全为1.(或者是只要有一个操作数为1,结果就为1)。
按位异或(^)
按位异或运算规则:
操作数1 | 0 | 0 | 1 | 1 |
操作数2 | 0 | 1 | 0 | 1 |
按位异或 | 0 | 1 | 1 | 0 |
规则总结:相异为1,相同为0
一个int4个字节,32位,如果让每一个二进制位拥有特殊含义,那么用一个值就能实现多种状态的压缩,如下图所示,实际上这种方式运用的很广,就是位图bitmap的思想,redis的bitmap结构,布隆过滤器都是用了类似的设计。

下面进行实战:
首先我们先定义每个位置上的状态表示什么

先定义好:
1 :第一位表示是否公开
1 << 1 = 2 :第二位表示是否顶置
1 << 2 = 4 : 第三位表示是否原创
定义的化需要用2的整数幂表示,因为整数幂只有自己位为1,其他位为0,方便操作。
那么我们如果想用一个值同时表示上面三个状态该怎么处理呢?

将定义的3个状态值进行或处理,或操作就是将1进行汇聚,如上图,第一位,第二位,第三位的1进行或操作后,变成的值三位都是1,那么1|2|4 = 7,这个7就能表示既公开,也顶置,也原创。
如果我从数据库读取的值为7,那么我怎么知道他包含哪几个状态呢?

如果我们想验证7这个状态是否顶置,该怎么办,就可以将7与我们想要验证的属性(也就是定义的1 << 1 = 2)进行&操作,判断结果是否为想验证的值,如果等于那么就表示顶置,如果不等于,那么就是不顶置,原理见上图。不明白的话,可以多缕缕。
现在7已经表示,公开,顶置,原创了,如果我想要修改去除某个属性怎么办呢?比如想去除顶置属性。

很简单,将7与想移除的属性,进行^操作,异或是相异为1,相同为0,其实就是把目标位的1变成0,也就移除掉了,同时不影响其他位。
实体类Article:
@ToString
@EqualsAndHashCode
@Builder
public class Article {
@Getter
@Setter
private Integer id;
@Getter
@Setter
private String articleName;
@Getter
@Setter
private String articleDesc;
@Getter
@Setter
private LocalDateTime createTime;
@Getter
@Setter
private Integer status;
private final boolean articlePublic;
private final boolean articleUp;
private final boolean articleOriginal;
private final boolean articleHot;
private final boolean articleRecommend;
public boolean isArticlePublic() {
return ArticleStatus.hasStatus(this.status,ArticleStatus.ARTICLE_PUBLIC);
}
public boolean isArticleUp() {
return ArticleStatus.hasStatus(this.status,ArticleStatus.ARTICLE_UP);
}
public boolean isArticleOriginal() {
return ArticleStatus.hasStatus(this.status,ArticleStatus.ARTICLE_ORIGINAL);
}
public boolean isArticleHot() {
return ArticleStatus.hasStatus(this.status,ArticleStatus.ARTICLE_HOT);
}
public boolean isArticleRecommend() {
return ArticleStatus.hasStatus(this.status,ArticleStatus.ARTICLE_RECOMMEND);
}
}使用了status表示所有状态,数据库也只存储这个字段,下面的articlePublic, articleUp,articleOriginal 等字段不进入数据库存储,写在这里是为了使用方便,且定义为final,没写set方法,只在get的时候,通过ArticleStatus类将 status 进行转换。
定义枚举ArticleStatus,并写了一些添加状态,判断,移除状态的方法。
@Getter
public enum ArticleStatus {
ARTICLE_PUBLIC(1,"公开文章"),
ARTICLE_UP(1 << 1,"顶置文章"),
ARTICLE_ORIGINAL(1 << 2,"原创文章"),
ARTICLE_HOT(1 << 3,"hot文章"),
ARTICLE_RECOMMEND(1 << 4,"推荐文章"),
;
private int code;
private String message;
ArticleStatus(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 检查是否存在某个属性
*/
public static Boolean hasStatus(Integer status, ArticleStatus articleStatus){
return (articleStatus.code & status) == articleStatus.code;
}
/**
* 在原来的基础上添加某种属性
*/
public static int addStatus(Integer status, ArticleStatus articleStatus){
if(hasStatus(status, articleStatus)){
return status;
}
return (status | articleStatus.code);
}
/**
* 移除某种属性
*/
public static int removeStatus(Integer status, ArticleStatus articleStatus) {
if(!hasStatus(status, articleStatus)){
return status;
}
return status ^ articleStatus.code;
}
/**
* 获取当前值包含的所有状态信息
*/
public static List binaryToList(int status) {
return Arrays.stream(values()).filter(articleStatus -> hasStatus(status, articleStatus))
.map(ArticleStatus::getMessage).collect(Collectors.toList());
}
}测试:
public static void main(String[] args) {
/**
* 初始化状态为公开文章
*/
int status = ArticleStatus.ARTICLE_PUBLIC.getCode();
// 增加顶置属性,原创属性,热文属性
status = ArticleStatus.addStatus(status,ArticleStatus.ARTICLE_UP);
status = ArticleStatus.addStatus(status,ArticleStatus.ARTICLE_ORIGINAL);
status = ArticleStatus.addStatus(status,ArticleStatus.ARTICLE_HOT);
Article article = Article.builder()
.id(1)
.articleName("测试文章")
.articleDesc("描述信息")
.createTime(LocalDateTime.now())
.status(status)
.build();
System.out.println("入库文章信息:" + article);
System.out.println("入库文章包含的属性:" + ArticleStatus.binaryToList(article.getStatus()));
// 移除文章的公开属性
status = ArticleStatus.removeStatus(status,ArticleStatus.ARTICLE_PUBLIC);
article.setStatus(status);
System.out.println("更新的文章信息:" + article);
System.out.println("更新的文章包含的属性:" + ArticleStatus.binaryToList(article.getStatus()));
}输出结果:
入库文章信息:Article(id=1, articleName=测试文章, articleDesc=描述信息, createTime=2021-04-26T15:10:10.643, status=15, articlePublic=true, articleUp=true, articleOriginal=true, articleHot=true, articleRecommend=false) 入库文章包含的属性:[公开文章, 顶置文章, 原创文章, hot文章] 更新的文章信息:Article(id=1, articleName=测试文章, articleDesc=描述信息, createTime=2021-04-26T15:10:10.643, status=14, articlePublic=false, articleUp=true, articleOriginal=true, articleHot=true, articleRecommend=false) 更新的文章包含的属性:[顶置文章, 原创文章, hot文章]
可以看到,我们只用了一个字段,就很方便的管理了5个字段属性,通过工具类进行状态添加,状态移除,状态判断等,在表数据量很大时,这种方式就节省了很多空间。
本文通过原理,实际代码等讲解了位运算在实际项目中的应用,这只是其中一个场景,在一些流程状态中,会有很多状态,且这些状态有先后顺序的,就可以用这种方式记录已执行的状态;
再比如,权限系统中,可以用多个int表示多个权限组(可以理解为角色),每个组有自己权限,每个人可以属于多个组,等等,也可以用到这种思想;
或者多个业务组存在多个业务,也是上面类似的思路。总之,就是位图的思想,状态压缩,一个值可以表示多种状态,而且如果状态超过32,可以用long表示。
版权声明:本文由星尘阁原创出品,转载请注明出处!