网站首页 文章专栏 高性能的位运算,以及在实际项目中的运用
比如就拿我自己博客网站来说,我的每一篇文章都有,是否顶置,是否公开,是否原创,是否热文,是否推荐等属性,那么对应的我就要在数据库中新建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表示。
版权声明:本文由星尘阁原创出品,转载请注明出处!