1. 程式人生 > 其它 >當Java列舉遇到位掩碼,還能這麼玩?

當Java列舉遇到位掩碼,還能這麼玩?

前言

相信大家都用過 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 的處理也是基於這個列舉序數+位掩碼的邏輯,二者實現一模一樣,算是一種主流的做法吧