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




























