当枚举遇到位掩码,还能这么玩?


theme: smartblue

先点赞再看,养成好习惯

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动连接java

前言

相信你们都用过 Fastjson,阿里的一个开源 JSON 库,在阿里系的开源项目里应用的很是普遍。虽然有时候也被戏称“沉的快”,但 Fastjson 从功能丰富度、易用性、源码设计角度来看,都是一款十分优秀的工具库。json

在使用 Fastjson 时,常常会配置一些枚举参数,好比日期格式、格式化输出、NULL值格式啊等等,就像下面这样配置:数组

String jsonStr = JSON.toJSONString(obj, 
                      SerializerFeature.WriteDateUseDateFormat,
                      SerializerFeature.PrettyFormat,
                      SerializerFeature.WriteNullStringAsEmpty);

// JSON.toJSONString
public static String toJSONString(Object object, SerializerFeature... features);

这种配置方式用起来那是至关爽,要什么输出配置,利用 JAVA 的可变长度参数(varargs)直接在方法后面动态追加就行;要是再加上 import static,那么连 SerializerFeature 都不用写了。工具

不过细想一下,这种动态传的参数数组,Fastjson 在接受后怎么知道咱们具体传了哪些参数?接受时遍历数组,每次 equals 对比吗?好比这样:post

// 写三个 for 循环的缘由是大几率下,不一样 feature 处理时机不一样,因此不能在一个 for 循环内处理

for (SerializerFeature feature : features) {
    if(feature.equals(SerializerFeature.WriteDateUseDateFormat)){
        // solve WriteDateUseDateFormat
    }
}

for (SerializerFeature feature : features) {
    if(feature.equals(SerializerFeature.PrettyFormat)){
        // solve PrettyFormat
    }
}

for (SerializerFeature feature : features) {
    if(feature.equals(SerializerFeature.WriteNullStringAsEmpty)){
        // solve WriteNullStringAsEmpty
    }
}

这样也太不“优雅”了,每次还须要遍历,白白浪费性能!或者只用一个循环,弄几个变量存储这几个 boolean 值呢:性能

boolean writeDateUseDateFormatEnable = false;
boolean PrettyFormatEnable = false;
boolean WriteNullStringAsEmptyEnable = false;

for (SerializerFeature feature : features) {
    if(feature.equals(SerializerFeature.WriteDateUseDateFormat)){
        writeDateUseDateFormatEnable = true;
    }
    if(feature.equals(SerializerFeature.PrettyFormat)){
        PrettyFormatEnable = true;
    }
    if(feature.equals(SerializerFeature.WriteNullStringAsEmpty)){
        WriteNullStringAsEmptyEnable = true;
    }
}

这样比上面要好一点了,但仍是须要循环判断,并且对每一个 Feature 都要额外增长一个变量来存储,同样不太“优雅”。spa

在 Fastjson 中使用了一种很巧妙的方式来处理这个动态的枚举参数设计

枚举中的序数(ordinal)

在正式介绍以前,须要先了解枚举中的一个概念- 序数(ordinal),每一个枚举类都会有一个 ordinal 属性,这个ordinal 表明的是当前枚举值在枚举类中的序号。好比下面这个枚举类里,F_A
/F_B
/F_C
/F_D 四个枚举值的序数依次为 0/1/2/3code

public enum Feature {
    F_A, // ordinal 0
    F_B, // ordinal 1
    F_C, // ordinal 2
    F_D, // ordinal 3
    ;
}

经过 ordinal() 方法,就能够获取枚举实例的序数值,好比Feature._F_A_.ordinal()orm

Fastjson 中的妙用

了解了枚举序数以后,如今来看看 Fastjson 中是怎么个玩法。在 SerializerFeature 的源码中有一个 mask(掩码) ,这个 mask 的值为 1 << ordinal

枚举中的位掩码 - Mask

public enum SerializerFeature {
   WriteDateUseDateFormat,
   PrettyFormat,
   WriteNullStringAsEmpty
    ...

    SerializerFeature(){
        mask = (1 << ordinal());
    }

    public final int mask;
    
    ...
}

在位运算中掩码 mask 的做用通常是为了保持/更改/删除某(些)位的值,有张图很是形象(这个图能够简单的理解为,白色像素表明1,黑色像素表明0,按为与后,为1的像素位才会显示):

image.png

那在 SerializerFeature 中,WriteDateUseDateFormat,
PrettyFormat,
WriteNullStringAsEmpty 三个值得序数分别为 0/1/2,左移后他们对应的二进制位以下:

0 0 0 1 WriteDateUseDateFormat
0 0 1 0 PrettyFormat
0 1 0 0 WriteNullStringAsEmpty

...
1 0 0 0

这里的作法很巧妙,用 1 左移序数个位,就能够获得一个序数位为 1 的数字,好比序数为 1 ,那么第0位就是1,序数为3,那么第4为就是1,以此类推,这样枚举中每一个值的 mask 里为 1 的位都会不一样

多个配置的处理

单看这个位掩码仍是以为没啥用,来看看实战吧。如今定义一个初始为 0 的 features 变量,用来存储全部的 feature

int features = 0;

利用位或(OR)对 features 和 mask 进行运算

features |= SerializerFeature.WriteDateUseDateFormat.getMask();

      0 0 0 0 [input(features)]
(|)   0 0 0 1 [mask(feature mask)]
-------------
      0 0 0 1 [output(new features)]

位或运算后的 features 为 0 0 0 1,第 0 位上变成了 1 ,表明第 1 位的枚举值(WriteDateUseDateFormat)被启用了,接着继续对 PrettyFormat 也执行位或,

features |= SerializerFeature.PrettyFormat.getMask();
0 0 0 1 [input(features)]
(|)   0 0 1 0 [mask(feature mask)]
-------------
      0 0 1 1 [output(new features)]

此时 features 为 0 0 1 1,第 2 位 上也变成了 1,表明第 2 位的枚举值(PrettyFormat)也被启用了

判断是否配置

有了 features 的值,仍是须要一个简单的判断方法,来检查某个枚举值是否被设置:

public static boolean isEnabled(int features, SerializerFeature feature) {
    return (features & feature.mask) != 0;
}

用 features 和 某个 Feature 的掩码位与后,就能够得出一个某位为 1 的数字。位与运算中只有上下两个位都为 1 ,返回的位才会为 1,那么只要返回的结果位内包含任何一个 1 ,这个数就不会为 0 ;因此只要这个结果不为 0 ,就能够说明这个 Feature 已经被设置了。

0 0 1 1 [input(features)]
(&)   0 0 1 0 [mask(PrettyFormat)]
-------------
      0 0 1 0 [output(new features)]

好比上面这个例子中,当前 features 为 0 0 1 1,和 PrettyFormat 的 mask 作位与后,就能够得出 0 0 1 0 ,结果不为 0 ,因此 PrettyFormat 已经被设置了

完整示例

// 存储全部配置的 Feature
int features = 0;

// 每添加一个 Feature, 就拿 features 和 当前 Feature 的掩码作位或运算
features |= SerializerFeature.WriteDateUseDateFormat.getMask();
features |= SerializerFeature.PrettyFormat.getMask();

// 再经过位与运算的结果,就能够判断某个 Feature 是否配置
boolean writeDateUseDateFormatEnabled = SerializerFeature.isEnabled(features,SerializerFeature.WriteDateUseDateFormat);
boolean prettyFormatEnabled = SerializerFeature.isEnabled(features,SerializerFeature.PrettyFormat);
boolean writeNullStringAsEmpty = SerializerFeature.isEnabled(features,SerializerFeature.WriteNullStringAsEmpty);

System.out.println("writeDateUseDateFormatEnabled: "+writeDateUseDateFormatEnabled);
System.out.println("prettyFormatEnabled: "+prettyFormatEnabled);
System.out.println("writeNullStringAsEmpty: "+writeNullStringAsEmpty);

//output
writeDateUseDateFormatEnabled: true
prettyFormatEnabled: true
writeNullStringAsEmpty: false

总结

不止是 Fastjson,Jackson 中对 Feature 的处理也是基于这个枚举序数+位掩码的逻辑,两者实现如出一辙,算是一种主流的作法吧。

参考

原创不易,禁止未受权的转载。若是个人文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤
相关文章
相关标签/搜索