1 定義
一種數據類型,只包含自定義的特定數據,是一組有共同特性的數據的集合。
創建需要enum關鍵字,如:
public enum Color{
RED, GREEN, BLUE, BLACK, PINK, WHITE;
}
enum的語法看似與類不同,但它實際上就是一個類。
把上面的編譯成 Gender.class, 然後用 javap -c Gender
反編譯
可得到
- Gender 是 final 的
- Gender 繼承自 java.lang.Enum 類
- 聲明瞭字段對應的兩個 static final Gender 的實例
- 實現了 values() 和 valueOf(String) 靜態方法
- static{} 對所有成員進行初始化
結合字節碼,還原 Gender 的普通類形式
public final class Gender extends java.lang.Enum {
public static final Gender Male;
public static final Gender Female;
private static final Gender[] $VALUES;
static {
Male = new Gender("Male", 0);
Female = new Gender("Female", 1);
$VALUES = new Gender[] {Male, Female};
}
public static Gender[] values() {
return $VALUE.clone();
}
public static Gender valueOf(String name) {
return Enum.valueOf(Gender.class, name);
}
}
創建的枚舉類型默認是java.lang.enum<枚舉類型名>(抽象類)的子類
每個枚舉項的類型都爲public static final 。
上面的那個類是無法編譯的,因爲編譯器限制了我們顯式的繼承自 java.Lang.Enum 類, 報錯 “The type Gender may not subclass Enum explicitly”, 雖然 java.Lang.Enum 聲明的是
這樣看來枚舉類其實用了多例模式,枚舉類的實例是有範圍限制的
它同樣像我們的傳統常量類,只是它的元素是有限的枚舉類本身的實例
它繼承自 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 相當於是
public static final 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 產生的所有字節碼,在此列出片斷
在 static{} 中大致看下 Male 的初始過程:加載 Gender$1, 並調用它的 Gender$1(java.lang.String, int) 構造函數生成一個 Gender$1 實例賦給 Male 屬性
既然enum是一個類,那麼它就可以像一般的類一樣擁有自己的屬性與方法。但Java要求必須先定義enum實例。
否則會編譯錯誤。
public enum Color {
RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
// 成員變量
private String name;
private int 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;
}
}
return null;
}
// 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;
}
}
枚舉實例的創建過程:枚舉類型符合通用模式 Class Enum<E extends Enum>,而 E 表示枚舉類型的名稱。枚舉類型的每一個值都將映射到 protected Enum(String name, int ordinal) 構造函數中,在這裏,每個值的名稱都被轉換成一個字符串,並且序數設置表示了此設置被創建的順序。
public enum 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 compareTo(E o) 比較此枚舉與指定對象的順序。
Class getDeclaringClass() 返回與此枚舉常量的枚舉類型相對應的 Class 對象。
String name() 返回此枚舉常量的名稱,在其枚舉聲明中對其進行聲明。
int ordinal() 返回枚舉常量的序數(它在枚舉聲明中的位置,其中初始常量序數爲零
String toString() 返回枚舉常量的名稱,它包含在聲明中。
static <T extends Enum> T valueOf(Class enumType, String name) 返回帶指定名稱的指定枚舉類型的枚舉常量。
二、常用用法
用法一:常量
在JDK1.5 之前,我們定義常量都是: public static fianl… 。現在好了,有了枚舉,可以把相關的常量分組到一個枚舉類型裏,而且枚舉提供了比常量更多的方法。
用法二:switch
JDK1.6之前的switch語句只支持int,char,enum類型,使用枚舉,能讓我們的代碼可讀性更強。
enum Color{
RED, GREEN, BLUE, BLACK, PINK, WHITE;
}
public class 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();
}
}
用法三:實現接口
public interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour {
RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
// 成員變量
private String name;
private int index;
// 構造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
// 接口方法
@Override
public String getInfo() {
return this.name;
}
// 接口方法
@Override
public void print() {
System.out.println(this.index + ":" + this.name);
}
}
枚舉集合
- EnumSet保證集合中的元素不重複
- EnumMap中的 key是enum類型,而value則可以是任意類型
三、綜合實例
最簡單的使用
最簡單的枚舉類
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
如何使用它呢?
先來看看它有哪些方法:
這是Weekday可以調用的方法和參數。發現它有兩個方法:values()和valueOf()。還有我們剛剛定義的七個變量
這些事枚舉變量的方法。我們接下來會演示幾個比較重要的
這段代碼,我們演示了幾個常用的方法和功能:
-
Weekday.valueOf() 方法:
它的作用是傳來一個字符串,然後將它轉變爲對應的枚舉變量。前提是你傳的字符串和定義枚舉變量的字符串一樣,區分大小寫。如果你傳了一個不存在的字符串,那麼會拋出異常。
-
Weekday.values()方法。
這個方法會返回包括所有枚舉變量的數組。在該例中,返回的就是包含了七個星期的Weekday[]。可以方便的用來做循環。
-
枚舉變量的toString()方法。
該方法直接返回枚舉定義枚舉變量的字符串,比如MON就返回【”MON”】。
-
枚舉變量的.ordinal()方法。
默認情況下,枚舉類會給所有的枚舉變量一個默認的次序,該次序從0開始,類似於數組的下標。而.ordinal()方法就是獲取這個次序(或者說下標)
-
枚舉變量的compareTo()方法。
該方法用來比較兩個枚舉變量的”大小”,實際上比較的是兩個枚舉變量的次序,返回兩個次序相減後的結果,如果爲負數,就證明變量1”小於”變量2 (變量1.compareTo(變量2),返回【變量1.ordinal() - 變量2.ordinal()】)
這是compareTo的源碼,會先判斷是不是同一個枚舉類的變量,然後再返回差值。
-
枚舉類的name()方法。
它和toString()方法的返回值一樣,事實上,這兩個方法本來就是一樣的:
這兩個方法的默認實現是一樣的,唯一的區別是,你可以重寫toString方法。name變量就是枚舉變量的字符串形式。
還有一些其他的方法我就暫時不介紹了,感興趣的話可以自己去看看文檔或者源碼,都挺簡單的。
要點:
- 使用的是enum關鍵字而不是class。
- 多個枚舉變量直接用逗號隔開。
- 枚舉變量最好大寫,多個單詞之間使用”_”隔開(比如:INT_SUM)。
- 定義完所有的變量後,以分號結束,如果只有枚舉變量,而沒有自定義變量,分號可以省略(例如上面的代碼就忽略了分號)。
- 在其他類中使用enum變量的時候,只需要【類名.變量名】就可以了,和使用靜態變量一樣。
但是這種簡單的使用顯然不能體現出枚舉的強大,我們來學習一下複雜的使用:
枚舉的高級使用方法
就像我們前面的案例一樣,你需要讓每一個星期幾對應到一個整數,比如星期天對應0。上面講到了,枚舉類在定義的時候會自動爲每個變量添加一個順序,從0開始。
假如你希望0代表星期天,1代表週一。。。並且你在定義枚舉類的時候,順序也是這個順序,那你可以不用定義新的變量,就像這樣:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
這個時候,星期天對應的ordinal值就是0,週一對應的就是1,滿足你的要求。但是,如果你這麼寫,那就有問題了:
public enum Weekday {
MON,TUS,WED,THU,FRI,SAT,SUN
}
我吧SUN放到了最後,但是我還是希0代表SUN,1代表MON怎麼辦呢?默認的ordinal是指望不上了,因爲它只會傻傻的給第一個變量0,給第二個1。。。
所以,我們需要自己定義變量!
看代碼:
我們對上面的代碼做了一些改變:
首先,我們在每個枚舉變量的後面加上了一個括號,裏面是我們希望它代表的數字。
然後,我們定義了一個int變量,然後通過構造函數初始化這個變量。
你應該也清楚了,括號裏的數字,其實就是我們定義的那個int變量。這句叫做自定義變量。
請注意:這裏有三點需要注意:
一定要把枚舉變量的定義放在第一行,並且以分號結尾。
構造函數必須私有化。事實上,private是多餘的,你完全沒有必要寫,因爲它默認並強制是private,如果你要寫,也只能寫private,寫public是不能通過編譯的。
自定義變量與默認的ordinal屬性並不衝突,ordinal還是按照它的規則給每個枚舉變量按順序賦值。
好了,你很聰明,你已經掌握了上面的知識,你想,既然能自定義一個變量,能不能自定義兩個呢?
當然可以:
你可以定義任何你想要的變量。學完了這些,大概枚舉類你也應該掌握了,但是,還有沒有其他用法呢?
枚舉類中的抽象類
如果我在枚舉類中定義一個抽象方法會怎麼樣?
你要知道,枚舉類不能繼承其他類,也不能被其他類繼承。至於爲什麼,我們後面會說到。
你應該知道,有抽象方法的類必然是抽象類,抽象類就需要子類繼承它然後實現它的抽象方法,但是呢,枚舉類不能被繼承。。你是不是有點亂?
我們先來看代碼:
你好像懂了點什麼。但是你好像又不太懂。爲什麼一個變量的後邊可以帶一個代碼塊並且實現抽象方法呢?
彆着急,帶着這個疑問,我們來看一下枚舉類的實現原理。
枚舉類的實現原理
從最簡單的看起:
還是這段熟悉的代碼,我們編譯一下它,再反編譯一下看看它到底是什麼樣子的:
你是不是覺得很熟悉?反編譯出來的代碼和我們用靜態變量自己寫的類出奇的相似!
而且,你看到了熟悉的values()方法和valueOf()方法。
仔細看,這個類繼承了java.lang.Enum類!所以說,枚舉類不能再繼承其他類了,因爲默認已經繼承了Enum類。
並且,這個類是final的!所以它不能被繼承!
回到我們剛纔的那個疑問:
爲什麼會有這麼神奇的代碼?現在你差不多懂了。因爲RED本身就是一個TrafficLamp對象的引用。實際上,在初始化這個枚舉類的時候,你可以理解爲執行的是TrafficLamp RED = new TrafficLamp(30)
,但是因爲TrafficLamp裏面有抽象方法,還記得匿名內部類麼?
我們可以這樣來創建一個TrafficLamp引用:
而在枚舉類中,我們只需要像上面那樣寫【RED(30){}
】就可以了,因爲java會自動的去幫我們完成這一系列操作
枚舉類的其他用法
雖然枚舉類不能繼承其他類,但是還是可以實現接口的
使用枚舉創建單例模式
使用枚舉創建的單例模式:
public enum EasySingleton{
INSTANCE;
}
代碼就這麼簡單,你可以使用EasySingleton.INSTANCE調用它,比起你在單例中調用getInstance()方法容易多了。
我們來看看正常情況下是怎樣創建單例模式的:
用雙檢索實現單例:
下面的代碼是用雙檢索實現單例模式的例子,在這裏getInstance()方法檢查了兩次來判斷INSTANCE是否爲null,這就是爲什麼叫雙檢索的原因,記住雙檢索在java5之前是有問題的,但是java5在內存模型中有了volatile變量之後就沒問題了。
public class DoubleCheckedLockingSingleton{
private volatile 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()來獲得實例對象。
用靜態工廠方法實現單例:
public class Singleton{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return INSTANCE;
}
}
你可以調用Singleton.getInstance()方法來獲得實例對象。
上面的兩種方式就是懶漢式和惡漢式單利的創建,但是無論哪一種,都不如枚舉來的方便。而且傳統的單例模式的另外一個問題是一旦你實現了serializable接口,他們就不再是單例的了。但是枚舉類的父類【Enum類】實現了Serializable接口,也就是說,所有的枚舉類都是可以實現序列化的,這也是一個優點。
總結
可以創建一個enum類,把它看做一個普通的類。除了它不能繼承其他類了。(java是單繼承,它已經繼承了Enum),可以添加其他方法,覆蓋它本身的方法
switch()參數可以使用enum
values()方法是編譯器插入到enum定義中的static方法,所以,當你將enum實例向上轉型爲父類Enum是,values()就不可訪問了。解決辦法:在Class中有一個getEnumConstants()方法,所以即便Enum接口中沒有values()方法,我們仍然可以通過Class對象取得所有的enum實例
無法從enum繼承子類,如果需要擴展enum中的元素,在一個接口的內部,創建實現該接口的枚舉,以此將元素進行分組。達到將枚舉元素進行分組。
enum允許程序員爲eunm實例編寫方法。所以可以爲每個enum實例賦予各自不同的行爲。