你真的會用枚舉嗎?
今天我們來聊聊枚舉。你可能會想:枚舉那么簡單,有什么好討論的?
沒錯,枚舉確實是一個極為常見的知識點,常見到很多人會忽略它,而只關注它最簡單的用法。當然,這和枚舉誕生的初衷有關。在 JDK1.5 以前,那是個沒有枚舉的時代,人們通過 public static final 的常量來定義一些全局使用的標識。甚至到現在,枚舉已經誕生很長時間了,但仍有一些人在使用這種方案。而且,在 C 語言中也有類似的“宏”的概念,如果你只是用來做全局標識,那么枚舉的意義就沒有那么大了。
但是,你真的了解枚舉嗎?今天我們就分別從實現原理、數據結構和設計模式 3 個方向來重新認識一下枚舉。
枚舉原理
我們知道一個枚舉的定義非常簡單。如果只考慮其作為標識的場景,那么從實現成本來看,枚舉和 public static final 的傳統方式差不多,甚至前者還更簡單些。
public enum Test{
A,B
}每一個枚舉成員都可以看作是枚舉類的實例,上面的 Test.A 的類型也是 Test。
Test t=Test.A;上面這個賦值語句看上去很簡單,仔細思考里面包含了幾層意思。首先左邊是 Test 類型的實例 t,那么右邊必然是一個類的實例。但是 Test.A 看上去像是一個類,這里很容易混淆。請注意,Test.A 是一個對象,不要被這里的大寫忽悠了,它不是類。
我們把這個枚舉翻譯成下面的樣子你是不是更熟悉?
Test A=new Test();
Test B=new Test();Java 枚舉類型的實現是在編譯階段進行的。這個階段和泛型的實現一樣,也就是說對 JVM 來說執行的字節碼集合并沒有增加任何新的指令,只是在 Java 代碼的層面加了一些語法。說白了,就是對已有的 JVM 指令集加了一層皮。
舉個生活化的例子,“進食”是人類最基本的行為,酒店會說“用餐”,但“用餐”是人體的新功能嗎?并不是。在計算機界這叫“語法糖”,看著很神奇,寫著也很爽,但底層還是老的功能。
我們可以對 Test.class 文件進行反編譯,注意反編譯命令是 javap,其中-p 的意思是反編譯的時候要包含私有方法。
javap -p Test.class輸出結果為:
public final class Test extends java.lang.Enum<Test> {
public static final Test A;
public static final Test B;
public static Test[] values();
public static Test valueOf(java.lang.String);
private Test();
static {};
}我們可以看到,確實 A 和 B 都是 Test 類的實例。Test 繼承了 java.lang.Enum 類,這里還有一個 Test 的無參構造方法,這里的 A 和 B 分別使用這個構造方法來實例化。
而實例化的過程發生在哪呢?
我們注意到上面代碼段的最后有一個空的 static 語句塊,我們可以基于 javap 的其他參數進一步分析 static 里面的字節碼內容。static 里面其實包含了很多字節碼指令,正是這些指令在做 A、B 的初始化工作。而 static 代碼塊是在類加載的時候執行的。也就是說當 Test 被加載的時候,A、B 就被初始化了。
static 內部除了完成初始化 A、B,還創建了一個名叫 ENUM$VALUES 的數組,然后把 A、B 按照定義的順序放入這個數組中。最后我們可以通過 values 方法來訪問這個數組。
這里我們可以增加一個構造方法,這樣大家就比較熟悉了。A、B 后面可以加一個括號調用這個構造方法。
public enum Test{
A("a"),B("b");
Test(String name){
this.name=name;
}
private String name;
}上面 A(“a”) 相當于如下代碼的簡寫:
Test A=new Test("a");我們再把例子做的復雜一些,我們為 Test 增加一個抽象方法 print。這里就不能像上面那樣直接初始化了。這里必須使用類似匿名內部類的寫法。
public enum Test{
A("a"){
@Override
public void print(){
System.out.print("a");
}
},
B("b"){
@Override
public void print(){
System.out.print("b");
}
};
Test(String name){
this.name=name;
}
private String name;
public abstract void print();
}上面這個寫法有點不倫不類,你需要適應一下。如果你去編譯目錄下查看文件,這次你會發現編譯后多了 Test和2.class 2 個文件,也就是匿名內部類。可見語法糖已經幫我們做好了一切。
數據結構
說完了枚舉的實現原理,我們再看看它支持的一些數據結構。常見的有 EnumSet 和 EnumMap。1.EnumSet
EnumSet 顯然是為枚舉打造的抽象集合類。它使用了位圖來存儲數據,因此非常緊湊。
EnumSet 有 2 個實現類,RegularEnumSet 和 JumboEnumSet。當我們創建 EnumSet 的時候,如果枚舉成員數量小于等于 64 將會使用 RegularEnumSet,大于 64 則會創建 JumboEnumSet。
為什么創建 EnumSet 的時候,會有不同的實現類呢?
這是因為 RegularEnumSet 采用 long 來存儲枚舉變量,而 long 是 64 位的,因此只能存儲 64 個變量。而 JumboEnumSet 使用 long[]來存儲枚舉變量,因此沒有這個限制。當然,你見過一個枚舉類超過 64 個成員變量嗎?如果真有這種情況,我認為放到 ZooKeeper 中會更合適一些。
這是一個很有意思的哲學問題,當你的枚舉變量只有 2 個,這個枚舉一般是很穩定的,但是你的枚舉變量超過了 64 個,我相信隨著業務發展枚舉數量還會新增,這種情況下就不適合用枚舉來解決了。也就是說枚舉變量越多,業務越不穩定。
EnumSet支持常見的集合操作,如取子集、增加、刪除、包含等。可以使用EnumSet的of方法來初始化。
EnumSet<Test> testSet = EnumSet.of(Test.A, Test.B);2.EnumMap
EnumMap 很明顯是一個 Map 結構,它的 key 就是枚舉,value 可以由你定義。比如下面這個聲明的意思就是對所有的 Object 按照 Test 枚舉類型來分類。其結果是輸出類型 A 及屬于 A 類型的對象,輸出類型 B 及屬于 B 類型的對象。
EnumMap<Test,List<Object>> testMap;設計模式
最后我們聊聊枚舉和設計模式的關系。
單例模式有很多實現方法,其中最好的就是用枚舉來實現。例如,下面的代碼段:
public enum Singleton{
INSTANCE;
private Singleton(){
//做一些初始化工作
}
}上面的 INSTANCE 就是我們的單例對象,我們可以把一些初始化工作放到 Singleton 的構造方法里面。還記得前面我們說的,枚舉的成員就是枚舉類的實例化對象,這個過程發生在 static 語句塊中。上面這段話所傳達的語義類似下面這樣:
Singleton INSTANCE=null;
static{
INSTANCE = new Singleton();
}此外枚舉在序列化和反序列化的時候并不會調用構造方法,這在一定程度上保障了單例。序列化僅僅是將枚舉對象的 name 屬性輸出到結果中,反序列化的時候則是通過 java.lang.Enum 的 valueOf 方法來根據名字查找枚舉對象。這里的處理和普通的類有很大的差異。
另外枚舉還可以實現模板模式、策略模式等。但是注意不要把太多代碼放到枚舉類,這樣不便于維護。關于用枚舉實現其他的設計模式讀者可以自己試試。
總結
上面就是枚舉的核心內容。我們知道枚舉本質上是一個語法糖,底層是通過繼承 java.lang.Enum 來實現的。枚舉的每個成員都是枚舉類的實例,并且還有自己的 Set 和 Map 數據結構,通過上面的分析我們可以看出枚舉底層實現很普通,但是很多語法特性超越了普通的 Java 類,在設計單例模式以及一些模板模式中將簡化編碼工作,使得工程整體變的更優雅、更緊湊。



























