一文看懂Java的枚舉

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()。還有我們剛剛定義的七個變量

這些事枚舉變量的方法。我們接下來會演示幾個比較重要的

這段代碼,我們演示了幾個常用的方法和功能:

  1. Weekday.valueOf() 方法:

    它的作用是傳來一個字符串,然後將它轉變爲對應的枚舉變量。前提是你傳的字符串和定義枚舉變量的字符串一樣,區分大小寫。如果你傳了一個不存在的字符串,那麼會拋出異常。

  2. Weekday.values()方法。

    這個方法會返回包括所有枚舉變量的數組。在該例中,返回的就是包含了七個星期的Weekday[]。可以方便的用來做循環。

  3. 枚舉變量的toString()方法。

    該方法直接返回枚舉定義枚舉變量的字符串,比如MON就返回【”MON”】。

  4. 枚舉變量的.ordinal()方法。

    默認情況下,枚舉類會給所有的枚舉變量一個默認的次序,該次序從0開始,類似於數組的下標。而.ordinal()方法就是獲取這個次序(或者說下標)

  5. 枚舉變量的compareTo()方法。

    該方法用來比較兩個枚舉變量的”大小”,實際上比較的是兩個枚舉變量的次序,返回兩個次序相減後的結果,如果爲負數,就證明變量1”小於”變量2 (變量1.compareTo(變量2),返回【變量1.ordinal() - 變量2.ordinal()】)

    這是compareTo的源碼,會先判斷是不是同一個枚舉類的變量,然後再返回差值。

  6. 枚舉類的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變量。這句叫做自定義變量。

請注意:這裏有三點需要注意:

  1. 一定要把枚舉變量的定義放在第一行,並且以分號結尾。

  2. 構造函數必須私有化。事實上,private是多餘的,你完全沒有必要寫,因爲它默認並強制是private,如果你要寫,也只能寫private,寫public是不能通過編譯的。

  3. 自定義變量與默認的ordinal屬性並不衝突,ordinal還是按照它的規則給每個枚舉變量按順序賦值。

好了,你很聰明,你已經掌握了上面的知識,你想,既然能自定義一個變量,能不能自定義兩個呢?

當然可以:

你可以定義任何你想要的變量。學完了這些,大概枚舉類你也應該掌握了,但是,還有沒有其他用法呢?

枚舉類中的抽象類

如果我在枚舉類中定義一個抽象方法會怎麼樣?

你要知道,枚舉類不能繼承其他類,也不能被其他類繼承。至於爲什麼,我們後面會說到。

你應該知道,有抽象方法的類必然是抽象類,抽象類就需要子類繼承它然後實現它的抽象方法,但是呢,枚舉類不能被繼承。。你是不是有點亂?

我們先來看代碼:

你好像懂了點什麼。但是你好像又不太懂。爲什麼一個變量的後邊可以帶一個代碼塊並且實現抽象方法呢?

彆着急,帶着這個疑問,我們來看一下枚舉類的實現原理。

枚舉類的實現原理

從最簡單的看起:

還是這段熟悉的代碼,我們編譯一下它,再反編譯一下看看它到底是什麼樣子的:

你是不是覺得很熟悉?反編譯出來的代碼和我們用靜態變量自己寫的類出奇的相似!

而且,你看到了熟悉的values()方法和valueOf()方法。

仔細看,這個類繼承了java.lang.Enum類!所以說,枚舉類不能再繼承其他類了,因爲默認已經繼承了Enum類。

並且,這個類是final的!所以它不能被繼承!

回到我們剛纔的那個疑問:

爲什麼會有這麼神奇的代碼?現在你差不多懂了。因爲RED本身就是一個TrafficLamp對象的引用。實際上,在初始化這個枚舉類的時候,你可以理解爲執行的是TrafficLamp RED = new TrafficLamp(30) ,但是因爲TrafficLamp裏面有抽象方法,還記得匿名內部類麼?

我們可以這樣來創建一個TrafficLamp引用:

而在枚舉類中,我們只需要像上面那樣寫【RED(30){}】就可以了,因爲java會自動的去幫我們完成這一系列操作

枚舉類的其他用法

 switch語句
雖然枚舉類不能繼承其他類,但是還是可以實現接口的
接口定義
實現接口
使用接口組織枚舉

使用枚舉創建單例模式

使用枚舉創建的單例模式:

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實例賦予各自不同的行爲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章