关于java:当Java枚举遇到位掩码还能这么玩

50次阅读

共计 4408 个字符,预计需要花费 12 分钟才能阅读完成。

置信大家都用过 Fastjson,阿里的一个开源 JSON 库,在阿里系的开源我的项目里利用的十分宽泛。尽管有时候也被戏称“沉的快”,但 Fastjson 从功能丰富度、易用性、源码设计角度来看,都是一款非常优良的工具库。

在应用 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 比照吗?比方这样:

// 写三个 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 都要额定减少一个变量来存储,一样不太“优雅”。

在 Fastjson 中应用了一种很奇妙的形式来解决这个动静的枚举参数

枚举中的序数(ordinal)

在正式介绍之前,须要先理解枚举中的一个概念 - 序数(ordinal),每个枚举类都会有一个 ordinal 属性,这个 ordinal 代表的是以后枚举值在枚举类中的序号。比方上面这个枚举类里,F_A /F_B /F_C /F_D 四个枚举值的序数顺次为 0/1/2/3

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

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

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 的像素位才会显示):

那在 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 的解决也是基于这个枚举序数 + 位掩码的逻辑,二者实现截然不同,算是一种支流的做法吧。

起源 | https://juejin.cn/post/6971567246774632462

正文完
 0