Java十六篇:枚舉
Java 枚舉(enum)
Java 枚舉是一個特殊的類,一般表示一組常量,比如一年的 4 個季節(jié),一個年的 12 個月份,一個星期的 7 天,方向有東南西北等,Java 枚舉類使用 enum 關鍵字來定義,各個常量使用逗號?,?來分割,通常將常量在接口中,在JDK1.5版本新增枚舉類型后就逐漸取代了這種常量定義方式
特點:
1.自定義的枚舉類都是繼承java.lang.Enum類的 2.枚舉類的每一個成員都是枚舉類的一個實例 3.枚舉類無法繼承,因為它已經繼承了java.lang.Enum類 4.在編譯時就能確定枚舉成員的類型 5.通常,將一組相關的常量值聚合在一起構建一個枚舉類。比如,顏色、星期、訂單類型、支付渠道等等一些含義相近的常量組合在一起創(chuàng)建一個枚舉類 單個的,彼此關聯性不大的一些值還是用常量比較好
枚舉的主要作用是用于定義有限個數對象的一種結構(多例設計),枚舉就屬于多例設計,并且其結構要比多例設計更加簡單。利用enum的關鍵字,可以實現枚舉的定義。在Java中枚舉類中可以定義構造方法、成員變量以及其他方法。
定義:
一種數據類型,只包含自定義的特定數據,是一組有共同特性的數據的集合。
創(chuàng)建需要enum關鍵字,如:
publicenum Color { ?
? ?RED, GREEN, BLUE, BLACK, PINK, WHITE; ?
}
或者
publicenum EnumTest {
? 標識符1[=整型常數],
? 標識符2[=整型常數],
? ...
? 標識符N[=整型常數],
}
enum的語法看似與類不同,但它實際上就是一個類。
把上面的編譯成 Gender.class, 然后用?javap -c Gender
反編譯
可得到
Gender 是 final 的
Gender 繼承自 java.lang.Enum 類
聲明了字段對應的兩個 static final Gender 的實例
實現了 values() 和 valueOf(String) 靜態(tài)方法
static{} 對所有成員進行初始化
結合字節(jié)碼,還原 Gender 的普通類形式
publicfinalclass Gender extends java.lang.Enum {
?publicstaticfinal Gender Male;
?publicstaticfinal Gender Female;
?privatestaticfinal Gender[] $VALUES;
?static {
? ?Male = new Gender("Male", 0);
? ?Female = new Gender("Female", 1);
? ?$VALUES = new Gender[] {Male, Female};
?}
?publicstatic Gender[] values() {
? ?return $VALUE.clone();
?}
?public static Gender valueOf(String name) {
? ?return Enum.valueOf(Gender.class, name);
?}
}
創(chuàng)建的枚舉類型默認是java.lang.enum<枚舉類型名>(抽象類)的子類
每個枚舉項的類型都為public static final。
上面的那個類是無法編譯的,因為編譯器限制了我們顯式的繼承自 java.Lang.Enum 類, 報錯 “The type Gender may not subclass Enum explicitly”, 雖然 java.Lang.Enum 聲明的是
這樣看來枚舉類其實用了多例模式,枚舉類的實例是有范圍限制的
它同樣像我們的傳統(tǒng)常量類,只是它的元素是有限的枚舉類本身的實例
它繼承自 java.lang.Enum, 所以可以直接調用 java.lang.Enum 的方法,如 name(), original() 等。
name 就是常量名稱
original 與 C 的枚舉一樣的編號
因為Java的單繼承機制,emum不能再用extends繼承其他的類。
可以在枚舉類中自定義構造方法,但必須是 private 或 package protected, 因為枚舉本質上是不允許在外面用 new Gender() 方式來構造實例的(Cannot instantiate the type Gender)
結合枚舉實現接口以及自定義方法,可以寫出下面那樣的代碼

方法可以定義成所有實例公有,也可以讓個別元素獨有
需要特別注明一下,上面在 Male {} 聲明一個 print() 方法后實際產生一個 Gender 的匿名子類,編譯后的 Gender$1,反編譯它
所以在 emum Gender 那個枚舉中的成員 Male 相當于是
publicstaticfinal Male = new Gender$1("Male", 0); //而不是 new Gender("Male", 0)
上面4: Invokespecial #1 要調用到下面的Gender(java.lang.String, int, Gender$1)方法
若要研究完整的 Male 元素的初始化過程就得 javap -c Gender 看 Gender.java 產生的所有字節(jié)碼,在此列出片斷
在 static{} 中大致看下 Male 的初始過程:加載 Gender并調用它的1(java.lang.String, int) 構造函數生成一個 Gender$1 實例賦給 Male 屬性
既然enum是一個類,那么它就可以像一般的類一樣擁有自己的屬性與方法。但Java要求必須先定義enum實例。
否則會編譯錯誤。
publicenum Color { ?
? ? ? ?RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4); ?
? ? ? ?// 成員變量 ?
? ? ? ?private String name; ?
? ? ? ?privateint index; ?
?
? ? ? ?// 構造方法 ?
? ? ? ?private Color(String name, int index) { ?
? ? ? ? ? ?this.name = name; ?
? ? ? ? ? ?this.index = index; ?
? ? ? ?} ?
?
? ? ? ?// 普通方法 ?
? ? ? ?public static String getName(int index) { ?
? ? ? ? ? ?for (Color c : Color.values()) { ?
? ? ? ? ? ? ? ?if (c.getIndex() == index) { ?
? ? ? ? ? ? ? ? ? ?return c.name; ?
? ? ? ? ? ? ? ?} ?
? ? ? ? ? ?} ?
? ? ? ? ? ?returnnull; ?
? ? ? ?} ?
?
? ? ? ?// get set 方法 ?
? ? ? ?public String getName() { ?
? ? ? ? ? ?return name; ?
? ? ? ?} ?
?
? ? ? ?public void setName(String name) { ?
? ? ? ? ? ?this.name = name; ?
? ? ? ?} ?
?
? ? ? ?public int getIndex() { ?
? ? ? ? ? ?return index; ?
? ? ? ?} ?
?
? ? ? ?public void setIndex(int index) { ?
? ? ? ? ? ?this.index = index; ?
? ? ? ?} ?
? ?}
枚舉實例的創(chuàng)建過程:枚舉類型符合通用模式 Class Enum,而 E 表示枚舉類型的名稱。枚舉類型的每一個值都將映射到 protected Enum(String name, int ordinal) 構造函數中,在這里,每個值的名稱都被轉換成一個字符串,并且序數設置表示了此設置被創(chuàng)建的順序。
publicenum Color{ ?
? ?RED, GREEN, BLUE, BLACK, PINK, WHITE; ?
}
相當于調用了六次Enum構造方法
Enum(“RED”, 0);
Enum(“GREEN”, 1);
Enum(“BLUE”, 2);
Enum(“BLACK”, 3);
Enum(“PINK”,4);
Enum(“WHITE”, 5);
枚舉類型的常用方法:
int ompareTo(E o) 比較此枚舉與指定對象的順序。
Class getDeclaringClass() 返回與此枚舉常量的枚舉類型相對應的 Class 對象。
String name() 返回此枚舉常量的名稱,在其枚舉聲明中對其進行聲明。
int ordinal() 返回枚舉常量的序數(它在枚舉聲明中的位置,其中初始常量序數為零
String toString() 返回枚舉常量的名稱,它包含在聲明中。
staticT valueOf(Class enumType, String name) 返回帶指定名稱的指定枚舉類型的枚舉常量。
常用方法
在JDK1.5 之前,我們定義常量都是:public static fianl… 。現在好了,有了枚舉,可以把相關的常量分組到一個枚舉類型里,而且枚舉提供了比常量更多的方法。
switch
JDK1.6之前的switch語句只支持int、char、enum類型,使用枚舉,能讓我們的代碼可讀性更強。
publicclass TestEnum { ?
? ?public void changeColor() { ?
? ?
? ? ? ?Color color = Color.RED;
? ? ? ?
? ? ? ?System.out.println("原色:" + color);
? ? ? ?
? ? ? ?switch(color){ ?
? ? ? ?case RED: ?
? ? ? ? ? ?color = Color.GREEN; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?case GREEN: ?
? ? ? ? ? ?color = Color.BLUE; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?case BLUE: ?
? ? ? ? ? ?color = Color.BLACK; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?case BLACK: ?
? ? ? ? ? ?color = Color.PINK; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?case PINK: ?
? ? ? ? ? ?color = Color.WHITE; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?case WHITE: ?
? ? ? ? ? ?color = Color.RED; ?
? ? ? ? ? ?System.out.println("變色:" + color); ?
? ? ? ? ? ?break; ?
? ? ? ?} ?
? ?} ?
? ?
? ?public static void main(String[] args){ ?
? ? ? ?TestEnum testEnum = new TestEnum(); ?
? ? ? ?testEnum.changeColor(); ?
? ?} ?
}
實現的接口
publicinterface Behaviour { ?
? ? ? ?void print(); ?
?
? ? ? ?String getInfo(); ?
? ?} ?
?
? ?publicenum Color implements Behaviour { ?
? ? ? ?RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4); ?
? ? ? ?// 成員變量 ?
? ? ? ?private String name; ?
? ? ? ?privateint index; ?
?
? ? ? ?// 構造方法 ?
? ? ? ?private Color(String name, int index) { ?
? ? ? ? ? ?this.name = name; ?
? ? ? ? ? ?this.index = index; ?
? ? ? ?} ?
?
? ? ? ?// 接口方法 ?
?
? ? ? ?@Override
? ? ? ?public String getInfo() { ?
? ? ? ? ? ?returnthis.name; ?
? ? ? ?} ?
?
? ? ? ?// 接口方法 ?
? ? ? ?@Override
? ? ? ?public void print() { ?
? ? ? ? ? ?System.out.println(this.index + ":" + this.name); ?
? ? ? ?} ?
? ?}
枚舉集合
EnumSet保證集合中的元素不重復
EnumMap中的 key是enum類型,而value則可以是任意類型
枚舉的高級常用方法
就像我們前面的案例一樣,你需要讓每一個星期幾對應到一個整數,比如星期天對應0。上面講到了,枚舉類在定義的時候會自動為每個變量添加一個順序,從0開始。
假如你希望0代表星期天,1代表周一。。。并且你在定義枚舉類的時候,順序也是這個順序,那你可以不用定義新的變量,就像這樣:
publicenum Weekday {
? ?SUN,MON,TUS,WED,THU,FRI,SAT
}
這個時候,星期天對應的ordinal值就是0,周一對應的就是1,滿足你的要求。但是,如果你這么寫,那就有問題了:
publicenum Weekday {
? ?MON,TUS,WED,THU,FRI,SAT,SUN
}
我吧SUN放到了最后,但是我還是希0代表SUN,1代表MON怎么辦呢?默認的ordinal是指望不上了,因為它只會傻傻的給第一個變量0,給第二個1。。。
所以,我們需要自己定義變量!
看代碼:
我們對上面的代碼做了一些改變:
首先,我們在每個枚舉變量的后面加上了一個括號,里面是我們希望它代表的數字。
然后,我們定義了一個int變量,然后通過構造函數初始化這個變量。
你應該也清楚了,括號里的數字,其實就是我們定義的那個int變量。這句叫做自定義變量。
請注意:這里有三點需要注意:
一定要把枚舉變量的定義放在第一行,并且以分號結尾。
構造函數必須私有化。事實上,private是多余的,你完全沒有必要寫,因為它默認并強制是private,如果你要寫,也只能寫private,寫public是不能通過編譯的。
自定義變量與默認的ordinal屬性并不沖突,ordinal還是按照它的規(guī)則給每個枚舉變量按順序賦值。
好了,你很聰明,你已經掌握了上面的知識,你想,既然能自定義一個變量,能不能自定義兩個呢?
當然可以:
你可以定義任何你想要的變量。學完了這些,大概枚舉類你也應該掌握了,但是,還有沒有其他用法呢?
枚舉類中的抽象類
如果我在枚舉類中定義一個抽象方法會怎么樣?
你要知道,枚舉類不能繼承其他類,也不能被其他類繼承。至于為什么,我們后面會說到。
你應該知道,有抽象方法的類必然是抽象類,抽象類就需要子類繼承它然后實現它的抽象方法,但是呢,枚舉類不能被繼承。。你是不是有點亂?
我們先來看代碼:

你好像懂了點什么。但是你好像又不太懂。為什么一個變量的后邊可以帶一個代碼塊并且實現抽象方法呢?
別著急,帶著這個疑問,我們來看一下枚舉類的實現原理。
枚舉類的實現原理
從最簡單的看起:

還是這段熟悉的代碼,我們編譯一下它,再反編譯一下看看它到底是什么樣子的:
你是不是覺得很熟悉?反編譯出來的代碼和我們用靜態(tài)變量自己寫的類出奇的相似!
而且,你看到了熟悉的values()方法和valueOf()方法。
仔細看,這個類繼承了java.lang.Enum類!所以說,枚舉類不能再繼承其他類了,因為默認已經繼承了Enum類。
并且,這個類是final的!所以它不能被繼承!
回到我們剛才的那個疑問:

為什么會有這么神奇的代碼?現在你差不多懂了。因為RED本身就是一個TrafficLamp對象的引用。實際上,在初始化這個枚舉類的時候,你可以理解為執(zhí)行的是TrafficLamp RED = new TrafficLamp(30) ,但是因為TrafficLamp里面有抽象方法,還記得匿名內部類么?
我們可以這樣來創(chuàng)建一個TrafficLamp引用:

而在枚舉類中,我們只需要像上面那樣寫【RED(30){}
】就可以了,因為java會自動的去幫我們完成這一系列操作
枚舉類用法:
雖然枚舉類不能繼承其他類,但是還是可以實現接口的

使用枚舉創(chuàng)建單例模式
使用枚舉創(chuàng)建的單例模式:
publicenum EasySingleton{
? ?INSTANCE;
}
代碼就這么簡單,你可以使用EasySingleton.INSTANCE調用它,比起你在單例中調用getInstance()方法容易多了。
我們來看看正常情況下是怎樣創(chuàng)建單例模式的:
用雙檢索實現單例:
下面的代碼是用雙檢索實現單例模式的例子,在這里getInstance()方法檢查了兩次來判斷INSTANCE是否為null,這就是為什么叫雙檢索的原因,記住雙檢索在java5之前是有問題的,但是java5在內存模型中有了volatile變量之后就沒問題了。
publicclass DoubleCheckedLockingSingleton{
? ? privatevolatile DoubleCheckedLockingSingleton INSTANCE;
? ? private DoubleCheckedLockingSingleton(){}
? ? public DoubleCheckedLockingSingleton getInstance(){
? ? ? ? if(INSTANCE == null){
? ? ? ? ? ?synchronized(DoubleCheckedLockingSingleton.class){
? ? ? ? ? ? ? ?//double checking Singleton instance
? ? ? ? ? ? ? ?if(INSTANCE == null){
? ? ? ? ? ? ? ? ? ?INSTANCE = new DoubleCheckedLockingSingleton();
? ? ? ? ? ? ? ?}
? ? ? ? ? ?}
? ? ? ? }
? ? ? ? return INSTANCE;
? ? }
}
你可以訪問DoubleCheckedLockingSingleTon.getInstance()來獲得實例對象。
用靜態(tài)工廠方法實現單例:
publicclass Singleton{
? ?privatestaticfinal Singleton INSTANCE = new Singleton();
? ?private Singleton(){}
? ?public static Singleton getSingleton(){
? ? ? ?return INSTANCE;
? ?}
}
你可以調用Singleton.getInstance()方法來獲得實例對象。
上面的兩種方式就是懶漢式和惡漢式單利的創(chuàng)建,但是無論哪一種,都不如枚舉來的方便。而且傳統(tǒng)的單例模式的另外一個問題是一旦你實現了serializable接口,他們就不再是單例的了。但是枚舉類的父類【Enum類】實現了Serializable接口,也就是說,所有的枚舉類都是可以實現序列化的,這也是一個優(yōu)點。
總結:
1.使用的是enum關鍵字而不是class。
2.多個枚舉變量直接用逗號隔開。
3.枚舉變量最好大寫,多個單詞之間使用”_”隔開(比如:INT_SUM)。
4.定義完所有的變量后,以分號結束,如果只有枚舉變量,而沒有自定義變量,分號可以省略(例如上面的代碼就忽略了分號)。
5.在其他類中使用enum變量的時候,只需要【類名.變量名】就可以了,和使用靜態(tài)變量一樣。
可以創(chuàng)建一個enum類,把它看做一個普通的類。除了它不能繼承其他類了。(java是單繼承,它已經繼承了Enum),可以添加其他方法,覆蓋它本身的方法
switch()參數可以使用enum
values()方法是編譯器插入到enum定義中的static方法,所以,當你將enum實例向上轉型為父類Enum是,values()就不可訪問了。解決辦法:在Class中有一個getEnumConstants()方法,所以即便Enum接口中沒有values()方法,我們仍然可以通過Class對象取得所有的enum實例
6.無法從enum繼承子類,如果需要擴展enum中的元素,在一個接口的內部,創(chuàng)建實現該接口的枚舉,以此將元素進行分組。達到將枚舉元素進行分組。
7.enum允許程序員為eunm實例編寫方法。所以可以為每個enum實例賦予各自不同的行為。
