4
头图
Like it first, then watch it, develop a good habit

Preface

I believe everyone has used Fastjson , an open source JSON library of Ali, which is widely used in open source projects of Ali department. Although sometimes jokingly called "shen fast", Fastjson is an excellent tool library from the perspective of feature richness, ease of use, and source code design.

When using Fastjson, some enumeration parameters are often configured, such as date format, formatted output, NULL value format, etc., like the following configuration:

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

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

This configuration method is very cool to use. What output configuration you want, use JAVA's variable length parameters (varargs) to dynamically append directly to the method; if you add import static , then you don't even need to write SerializerFeature.

But if you think about it, how does Fastjson know which parameters we passed after receiving such a dynamically passed parameter array? Iterate over the array when accepting, do equals compare every time? For example:

// 写三个 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
    }
}

This is too "elegant", it needs to be traversed every time, and performance is wasted! Or just use a loop and make a few variables to store these boolean values:

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;
    }
}

This is better than the above, but it still needs to be cyclically judged, and an additional variable must be added to store each Feature, which is also not "elegant".

A very clever way is used in Fastjson to handle this dynamic enumeration parameter

Ordinal in enumeration (ordinal)

Before the formal introduction, you need to understand a concept in enumeration- ordinal (ordinal) , each enumeration class will have an ordinal attribute, this ordinal represents the serial number of the current enumeration value in the enumeration class . For example, in the following enumeration class, F_A
/F_B
/F_C
/F_D The ordinal of the four enumerated values is 0/1/2/3

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

Through the ordinal() method, you can get the ordinal value of the enumeration instance, such as Feature._F_A_.ordinal()

The magic in Fastjson

After understanding the enumeration ordinal, let's take a look at how to play in Fastjson. In the source code of SerializerFeature mask (mask) , the value of this mask is 1 << ordinal

Bit mask in enumeration-Mask

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

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

    public final int mask;
    
    ...
}

The role of mask in bit operation is generally to keep/change/delete the value of certain bit(s). There is a picture that is very vivid (this picture can be simply understood as white pixels represent 1, black pixels represent 0, press After being and, the pixel bits that are 1 will be displayed):

image.png

That is in SerializerFeature , WriteDateUseDateFormat,
PrettyFormat,
The ordinal numbers of WriteNullStringAsEmpty are 0/1/2 respectively. After shifting to the left, their corresponding binary bits are as follows:

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

...
1 0 0 0

Approach here is very subtle, with a left ordinal bits, it can get a sequence number of bit number 1 , such as sequence number 1, then the bit 0 is 1, sequence number 3, then the first 4 is 1, By analogy, the bit of 1 in the mask of each value in the enumeration will be different

Handling of multiple configurations

Just looking at this bitmask still feels useless, let's take a look at the actual combat. Now define a features variable with an initial value of 0 to store all features

int features = 0;

Use bitwise OR (OR) to perform operations on features and 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)]

The features after bit-OR operation are 0 0 0 1 , and the 0th bit becomes 1 to indicate that the first bit enumeration value (WriteDateUseDateFormat) is enabled, and then continue to perform bitwise OR on 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)]

At this time, the features is 0 0 1 1 , and the second position has also become 1, and the enumeration value (PrettyFormat) representing the second position is also enabled

Determine whether to configure

With the value of features, a simple judgment method is still needed to check whether an enumeration value is set:

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

After using features and the mask a Feature to do and , a number with a certain digit of 1 can be obtained. In the bitwise AND operation, only the upper and lower two bits are 1, the returned bit will be 1, so as long as the returned result bit contains any 1s, the number will not be 0; so as long as the result is not 0, then It can indicate that this Feature has been set.

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

For example, in the above example, the current features are 0 0 1 1 , and after bit- 0 0 1 0 , the result is not 0, so PrettyFormat has been set

Complete example

// 存储所有配置的 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

to sum up

Not only Fastjson, but the processing of Feature in Jackson is also based on the logic of enumeration ordinal + bit mask. The two implementations are exactly the same, which is a mainstream approach.

reference

Originality is not easy, unauthorized reprinting is prohibited. If my article is helpful to you, please like/favorite/follow to encourage and support it ❤❤❤❤❤❤

空无
3.3k 声望4.3k 粉丝

坚持原创,专注分享 JAVA、网络、IO、JVM、GC 等技术干货