Java枚舉類

背景

  • 在java語言中還沒有引入枚舉類型之前,表示枚舉類型的常用模式是聲明一組具有int常量。之前我們通常利用public final static 方法定義的代碼如下,分別用1 表示春天,2表示夏天,3表示秋天,4表示冬天
public class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}
  • 這種方法稱作int枚舉模式。可這種模式有什麼問題呢,我們都用了那麼久了,應該沒問題的。通常我們寫出來的代碼都會考慮它的安全性、易用性和可讀性。 首先我們來考慮一下它的類型安全性。當然這種模式不是類型安全的。比如說我們設計一個函數,要求傳入春夏秋冬的某個值。但是使用int類型,我們無法保證傳入的值爲合法。代碼如下所示:
private String getChineseSeason(int season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case Season.SPRING :
                result.append("春天");
                break;
            case Season.SUMMER :
                result.append("夏天");
                break;
            case Season.AUTUMN :
                result.append("秋天");
                break;
            case Season.WINTER :
                result.append("冬天");
                break;
            default :
                result.append("地球沒有的季節");
                break;
        }
        return result.toString();
    }

    public void doSomething(){
        System.out.println(this.getChineseSeason(Season.SPRING));//這是正常的場景

        System.out.println(this.getChineseSeason(5));
        //這個卻是不正常的場景,這就導致了類型不安全問題
    }
  • 程序getChineseSeason(Season.SPRING)是我們預期的使用方法。可getChineseSeason(5)顯然就不是了,而且編譯很通過,在運行時會出現什麼情況,我們就不得而知了。這顯然就不符合Java程序的類型安全

  • 接下來我們來考慮一下這種模式的可讀性。使用枚舉的大多數場合,我都需要方便得到枚舉類型的字符串表達式。如果將int枚舉常量打印出來,我們所見到的就是一組數字,這是沒什麼太大的用處。我們可能會想到使用String常量代替int常量。雖然它爲這些常量提供了可打印的字符串,但是它會導致性能問題,因爲它依賴於字符串的比較操作,所以這種模式也是我們不期望的。 從類型安全性和程序可讀性兩方面考慮,int和String枚舉模式的缺點就顯露出來了。幸運的是,從Java1.5發行版本開始,就提出了另一種可以替代的解決方案,可以避免int和String枚舉模式的缺點,並提供了許多額外的好處。那就是枚舉類型(enum type)。接下來的章節將介紹枚舉類型的定義、特徵、應用場景和優缺點

定義

  • 枚舉類型(enum type)是指由一組固定的常量組成合法的類型。Java中由關鍵字enum來定義一個枚舉類型。下面就是java枚舉類型的定義
public enum Season {
    SPRING, SUMMER, AUTUMN, WINER;
}

特點

Java定義枚舉類型的語句很簡約。它有以下特點:

  1. 使用關鍵字enum

  2. 類型名稱,比如這裏的Season

  3. 一串允許的值,比如上面定義的春夏秋冬四季

  4. 枚舉可以單獨定義在一個文件中,也可以嵌在其它Java類中

  5. 枚舉可以實現一個或多個接口(Interface)

  6. 可以定義新的變量

  7. 可以定義新的方法

  8. 可以定義根據具體枚舉值而相異的類

應用場景

  • 以在背景中提到的類型安全爲例,用枚舉類型重寫那段代碼。代碼如下:
public enum Season {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int code;
    private Season(int code){
        this.code = code;
    }

    public int getCode(){
        return code;
    }
}
public class UseSeason {
    /**
     * 將英文的季節轉換成中文季節
     * @param season
     * @return
     */
    public String getChineseSeason(Season season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case SPRING :
                result.append("[中文:春天,枚舉常量:" + season.name() + ",數據:" + season.getCode() + "]");
                break;
            case AUTUMN :
                result.append("[中文:秋天,枚舉常量:" + season.name() + ",數據:" + season.getCode() + "]");
                break;
            case SUMMER : 
                result.append("[中文:夏天,枚舉常量:" + season.name() + ",數據:" + season.getCode() + "]");
                break;
            case WINTER :
                result.append("[中文:冬天,枚舉常量:" + season.name() + ",數據:" + season.getCode() + "]");
                break;
            default :
                result.append("地球沒有的季節 " + season.name());
                break;
        }
        return result.toString();
    }

    public void doSomething(){
        for(Season s : Season.values()){
            System.out.println(getChineseSeason(s));//這是正常的場景
        }
        //System.out.println(getChineseSeason(5));
        //此處已經是編譯不通過了,這就保證了類型安全
    }

    public static void main(String[] arg){
        UseSeason useSeason = new UseSeason();
        useSeason.doSomething();
    }
}
  • 輸出

[中文:春天,枚舉常量:SPRING,數據:1] [中文:夏天,枚舉常量:SUMMER,數據:2] [中文:秋天,枚舉常量:AUTUMN,數據:3] [中文:冬天,枚舉常量:WINTER,數據:4]

  • 這裏有一個問題,爲什麼我要將域添加到枚舉類型中呢?目的是想將數據與它的常量關聯起來。如1代表春天,2代表夏天

什麼時候使用

  • 那麼什麼時候應該使用枚舉呢?每當需要一組固定的常量的時候,如一週的天數、一年四季等。或者是在我們編譯前就知道其包含的所有值的集合。Java 1.5的枚舉能滿足絕大部分程序員的要求的,它的簡明,易用的特點是很突出的

用法

  • 這裏介紹了七種常見的用法

常量

public enum Color {  
  RED, GREEN, BLANK, YELLOW  
}  

switch

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
}  

向枚舉中添加新方法

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;  
    }  
}  

覆蓋枚舉的方法

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;  
    }  
    //覆蓋方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  

實現接口

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);  
    }  
}  

使用接口組織枚舉

public interface Food {  
    enum Coffee implements Food{  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum Dessert implements Food{  
        FRUIT, CAKE, GELATO  
    }  
}

關於枚舉集合的使用

  • java.util.EnumSet和java.util.EnumMap是兩個枚舉集合。EnumSet保證集合中的元素不重複;EnumMap中的 key是enum類型,而value則可以是任意類型。關於這個兩個集合的使用就不在這裏贅述,可以參考JDK文檔


枚舉是如何保證線程安全的

  • 要想看源碼,首先得有一個類吧,那麼枚舉類型到底是什麼類呢?是enum嗎?答案很明顯不是,enum就和class一樣,只是一個關鍵字,他並不是一個類,那麼枚舉是由什麼類維護的呢,我們簡單的寫一個枚舉:
public enum t {
    SPRING,SUMMER,AUTUMN,WINTER;
}
  • 然後我們使用反編譯,看看這段代碼到底是怎麼實現的,反編譯(Java的反編譯)後代碼內容如下:
public final class T extends Enum{
    private T(String s, int i){
        super(s, i);
    }
    public static T[] values(){
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }

    public static T valueOf(String s){
        return (T)Enum.valueOf(demo/T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static{
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
  • 通過反編譯後代碼我們可以看到,public final class T extends Enum,說明,該類是繼承了Enum類的,同時final關鍵字告訴我們,這個類也是不能被繼承的。當我們使用enmu來定義一個枚舉類型的時候,編譯器會自動幫我們創建一個final類型的類繼承Enum類,所以枚舉類型不能被繼承,我們看到這個類中有幾個屬性和方法

  • 我們可以看到:

public static final T SPRING;
        public static final T SUMMER;
        public static final T AUTUMN;
        public static final T WINTER;
        private static final T ENUM$VALUES[];
        static
        {
            SPRING = new T("SPRING", 0);
            SUMMER = new T("SUMMER", 1);
            AUTUMN = new T("AUTUMN", 2);
            WINTER = new T("WINTER", 3);
            ENUM$VALUES = (new T[] {
                SPRING, SUMMER, AUTUMN, WINTER
            });
        }
  • 都是static類型的,因爲static類型的屬性會在類被加載之後被初始化,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的。所以,創建一個enum類型是線程安全的

爲什麼用枚舉實現的單例是最好的方式

  • 單例模式的七種寫法中,我們看到一共有七種實現單例的方式,其中,Effective Java作者Josh Bloch 提倡使用枚舉的方式,既然大神說這種方式好,那我們就要知道它爲什麼好?

枚舉寫法簡單

public enum EasySingleton{
    INSTANCE;
}
  • 你可以通過EasySingleton.INSTANCE來訪問

枚舉自己處理序列化

  • 我們知道,以前的所有的單例模式都有一個比較大的問題,就是一旦實現了Serializable接口之後,就不再是單例得了,因爲,每次調用 readObject()方法返回的都是一個新創建出來的對象,有一種解決辦法就是使用readResolve()方法來避免此事發生。但是,爲了保證枚舉類型像Java規範中所說的那樣,每一個枚舉類型及其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定。原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

  • 大概意思就是說,在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我們看一下這個valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  
  • 從代碼中可以看到,代碼會嘗試從調用enumType這個Class對象的enumConstantDirectory()方法返回的map中獲取名字爲name的枚舉對象,如果不存在就會拋出異常。再進一步跟到enumConstantDirectory()方法,就會發現到最後會以反射的方式調用enumType這個類型的values()靜態方法,也就是上面我們看到的編譯器爲我們創建的那個方法,然後用返回結果填充enumType這個Class對象中的enumConstantDirectory屬性。所以,JVM對序列化有保證

枚舉實例創建是thread-safe(線程安全的)

  • 當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的。所以,創建一個enum類型是線程安全的

擴展資料

參考資料

發佈了67 篇原創文章 · 獲贊 26 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章