java 枚舉(enum) 全面解讀

簡(jiǎn)介

枚舉是Java1.5引入的新特性,通過(guò)關(guān)鍵字enum來(lái)定義枚舉類。枚舉類是一種特殊類,它和普通類一樣可以使用構(gòu)造器、定義成員變量和方法,也能實(shí)現(xiàn)一個(gè)或多個(gè)接口,但枚舉類不能繼承其他類.

原理分析

枚舉類型使用的最常用類型就是枚舉常量.下面通過(guò)一個(gè)簡(jiǎn)單的Demo來(lái)說(shuō)明枚舉的原理.

// 定義
public enum Color {
    BLACK, WHITE
}

// 使用
public class Main {
    public static void main(String[] args) {
        System.out.println(Color.BLACK);
    }
}
// 結(jié)果
// BLACK

這樣只是能夠知道枚舉簡(jiǎn)單的使用方法,不能看出枚舉的特點(diǎn)和枚舉的具體實(shí)現(xiàn).
下面我們通過(guò) jad工具來(lái)反編譯Color類, 通過(guò)jad -sjava Color.class反編譯出一份java文件.

// final修飾,無(wú)法被繼承
public final class Color extends Enum {

    // 為了避免 返回的數(shù)組修改,而引起內(nèi)部values值的改變,返回的是原數(shù)組的副本
    public static Color[] values() {
        return (Color[]) $VALUES.clone();
    }

    // 按名字獲取枚舉實(shí)例
    public static Color valueOf(String name) {
        return (Color) Enum.valueOf(em / Color, name);
    }

    // 私有的構(gòu)造函數(shù)
    private Color(String name, int ordinal) {
        super(name, ordinal);
    }

    // enum第一行的聲明的變量,都對(duì)應(yīng)一個(gè)枚舉實(shí)例對(duì)象
    public static final Color BLACK;
    public static final Color WHITE;
    //
    private static final Color $VALUES[];

    // 靜態(tài)域初始化,說(shuō)明在類加載的cinit階段就會(huì)被實(shí)例化,jvm能夠保證類加載過(guò)程的線程安全
    static {
        BLACK = new Color("BLACK", 0);
        WHITE = new Color("WHITE", 1);
        $VALUES = (new Color[]{
                BLACK, WHITE
        });
    }
}

從反編譯的類中,可以看出, 我們使用enum關(guān)鍵字編寫的類,在編譯階段編譯器會(huì)自動(dòng)幫我們生成一份真正在jvm中運(yùn)行的代碼.

該類繼承自 Enum類,public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable.

Enum類接受一個(gè)繼承自Enum的泛型.(在反編譯java文件中沒(méi)有體現(xiàn)泛型是因?yàn)?泛型在階段就會(huì)被類型類型擦除,替換為具體的實(shí)現(xiàn).).

從反編譯的Color類中可以看出,在enum關(guān)鍵字的類中,第一行 (準(zhǔn)確的說(shuō)是第一個(gè)分號(hào)前) 定義的變量,都會(huì)生成一個(gè) Color實(shí)例,且它是在靜態(tài)域中進(jìn)行初始化的, 而靜態(tài)域在類加載階段的cinit中進(jìn)行初始化,所以枚舉對(duì)象是線程安全的,由JVM來(lái)保證.

生成的枚舉類有 Color $VALUES[];成員變量,外部可以通過(guò)values()方法獲取當(dāng)前枚舉類的所有實(shí)例對(duì)象.

Enum成員變量和方法分析

Enum成員變量和方法

Enum類實(shí)現(xiàn)了 Comparable接口,表明它是支持排序的,可以通過(guò) Collections.sort 進(jìn)行自動(dòng)排序.實(shí)現(xiàn)了public final int compareTo(E o)接口,方法定義為final且其實(shí)現(xiàn)依賴的ordinal字段也是final類型,說(shuō)明他只能根據(jù)ordinal排序,排序規(guī)則不可變.

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

ordinal: 表示枚舉的順序,從Color類中可以看出,它是從0開(kāi)始按自然數(shù)順序增長(zhǎng),且其值是final類型,外部無(wú)法更改.對(duì)于 ordinal()方法,官方建議盡量不要使用它,它主要是提供給EnumMap,EnumSet使用的.

/**
     * Returns the ordinal of this enumeration constant (its position
     * in its enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this method.  It is
     * designed for use by sophisticated enum-based data structures, such
     * as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     *
     * @return the ordinal of this enumeration constant
     */
    public final int ordinal() {
        return ordinal;
    }

name: 表示枚舉類的名字,從Color類的構(gòu)造函數(shù)可以看出,它的值就是我們定義的實(shí)例的名稱.
我們?cè)诶又兄阅艽蛴〕鰧?shí)例名稱,是因?yàn)?它的toString()方法直接返回了name屬性.

/**
     * Returns the name of this enum constant, as contained in the
     * declaration.  This method may be overridden, though it typically
     * isn't necessary or desirable.  An enum type should override this
     * method when a more "programmer-friendly" string form exists.
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }

equals(): 從其實(shí)現(xiàn)來(lái)看, 我們程序中使用 == 或者 equals來(lái)判斷兩個(gè)枚舉相等都是一樣的.

 public final boolean equals(Object other) {
        return this==other;
    }

getDeclaringClass(): 方法返回枚舉聲明的Class對(duì)象

每一個(gè)枚舉類型極其定義的枚舉變量在JVM中都是唯一的

這句話的意思是枚舉類型它擁有的實(shí)例在編寫的時(shí)候,就已經(jīng)確定下,不能通過(guò)其他手段進(jìn)行創(chuàng)建,且枚舉變量在jvm有且只有一個(gè)對(duì)應(yīng)的實(shí)例.

為了達(dá)到這個(gè)效果,它通過(guò)以下方法來(lái)確保.

  1. 類加載時(shí)創(chuàng)建,保證線程安全

Color類中可以看出, Color對(duì)象是在靜態(tài)域創(chuàng)建,由類加載時(shí)初始化,JVM保證線程安全,這樣就能確保Color對(duì)象不會(huì)因?yàn)椴l(fā)同時(shí)請(qǐng)求而錯(cuò)誤的創(chuàng)建多個(gè)實(shí)例.

  1. 對(duì)序列化進(jìn)行特殊處理,防止反序列化時(shí)創(chuàng)建新的對(duì)象

我們知道一旦實(shí)現(xiàn)了Serializable接口之后,反序列化時(shí)每次調(diào)用 readObject()方法返回的都是一個(gè)新創(chuàng)建出來(lái)的對(duì)象.

而枚舉則不同,在序列化的時(shí)候Java僅僅是將枚舉對(duì)象的name屬性輸出到結(jié)果中,反序列化的時(shí)候則是通過(guò)Enum的valueOf()方法來(lái)根據(jù)名字查找枚舉對(duì)象。同時(shí),編譯器是不允許任何對(duì)這種序列化進(jìn)行定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplacereadResolve等方法。

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 constant " + enumType.getCanonicalName() + "." + name);
    }
    /**
     * prevent default deserialization
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
  1. 私有構(gòu)造函數(shù), 無(wú)法正常的 new出對(duì)象
    // 私有的構(gòu)造函數(shù)
    private Color(String name, int ordinal) {
        super(name, ordinal);
    }
  1. 無(wú)法通過(guò) clone()方法,克隆對(duì)象
    /**
     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
  1. 無(wú)法通過(guò)反射的方式創(chuàng)建枚舉對(duì)象

枚舉類型,在 JVM 層面禁止了通過(guò)反射構(gòu)造枚舉實(shí)例的行為,如果嘗試通過(guò)反射創(chuàng)建,將會(huì)報(bào)Cannot reflectively create enum objects.

    static void reflectTest() throws Exception {
        // 獲取類對(duì)象
        Class<?> cls = Class.forName("em.Color");
        // 獲取 color 的構(gòu)造函數(shù)
        Constructor<?> constructor = cls.getDeclaredConstructor(String.class, int.class);
        // 獲取訪問(wèn)權(quán)限
        constructor.setAccessible(true);
        // 實(shí)例化
        Object reflectColor = constructor.newInstance("name", 0);
    }

    // 報(bào)錯(cuò)
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at Main.reflect(Main.java:24)
    at Main.main(Main.java:13)

枚舉類的特點(diǎn)總結(jié)

  1. 枚舉實(shí)例必須在 enum關(guān)鍵字聲明的類中顯式的指定(首行開(kāi)始的以第一個(gè)分號(hào)結(jié)束)
  2. 除了1, 沒(méi)有任何方式(new,clone,反射,序列化)可以手動(dòng)創(chuàng)建枚舉實(shí)例
  3. 枚舉類不可被繼承
  4. 枚舉類是線程安全的
  5. 枚舉類型是類型安全的(typesafe)
  6. 無(wú)法繼承其他類(已經(jīng)默認(rèn)繼承Enum)

枚舉的使用

  • 枚舉常量

如上訴 Color枚舉類,就是典型的枚舉常量.

它可以在 switch語(yǔ)句中使用

    void enmuTest() {
        Color tag = Color.BLACK;
        switch (tag) {
            case WHITE:
                break;
            case BLACK:
                break;
        }
    }

枚舉類型是類型安全的,可以對(duì)傳入的值進(jìn)行類型檢查:

如有個(gè) handleColor(Color color)方法,那么方法參數(shù)自動(dòng)會(huì)對(duì)類型進(jìn)行檢查,只能傳入 Color.WHITEColor.BLACK,如果使用 static final定義的常量則不具備 類型安全的特點(diǎn).

  • 枚舉與構(gòu)造函數(shù)

枚舉類可以編寫自己的構(gòu)造函數(shù),但是不能聲明public,protected,為了是不讓外部創(chuàng)建實(shí)例對(duì)象,默認(rèn)為private且只能為它.

  • 枚舉與類

除了枚舉常量外, enum是一個(gè)完整的類,它也可以編寫自己的構(gòu)造方法以及方法,甚至實(shí)現(xiàn)接口.

這里需要注意,枚舉類不能繼承其他類,因?yàn)樵诰幾g時(shí)它已經(jīng)繼承了 Enum,java無(wú)法多繼承

// 實(shí)現(xiàn)Runnable接口,在這個(gè)類中沒(méi)有意義,只是為了舉例
public enum Color implements Runnable {
    WHITE("黑色"),
    BLACK("白色");
    private final String value;

    // 自定義構(gòu)造,雖然沒(méi)有寫private,但是默認(rèn)就是private
    Color(String v) {
        value = v;
    }

    // 自定義方法
    public void draw() {
        System.out.println("繪制 " + value);
    }

    // 重寫方法
    @Override
    public String toString() {
        return value;
    }

    // 實(shí)現(xiàn)接口方法
    @Override
    public void run() {
        // todo ...
    }
}

枚舉與單例模式

單例模式網(wǎng)上有6-7中寫法,除了 枚舉方式外, 都有兩個(gè)致命的缺點(diǎn), 不能完全保證單例在jvm中保持唯一性.

  1. 反射創(chuàng)建單例對(duì)象

解決方案 : 在構(gòu)造上述中判斷,當(dāng)多于一個(gè)實(shí)例時(shí),再調(diào)用構(gòu)造函數(shù),直接報(bào)錯(cuò).

  1. 反序列化時(shí)創(chuàng)建對(duì)象

解決方案 : 使用readResolve()方法來(lái)避免此事發(fā)生.

這兩種缺點(diǎn)雖然都有方式解決,但是不免有些繁瑣.

枚舉類天生有這些特性.而且實(shí)現(xiàn)單例相當(dāng)簡(jiǎn)單.

public enum Singleton {
    INSTANCE;

    public void method() {
        // todo ...
    }
}

所以,枚舉實(shí)現(xiàn)的單例,可以說(shuō)是最完美和簡(jiǎn)潔的單例了.推薦大家使用這種方式創(chuàng)建單例.

但是,枚舉類的裝載和初始化時(shí)會(huì)有時(shí)間和空間的成本. 它的實(shí)現(xiàn)比其他方式需要更多的內(nèi)存空間,所以在Android這種受資源約束的設(shè)備中盡量避免使用枚舉單例,而選擇 雙重檢查鎖(DCL)靜態(tài)內(nèi)部類的方式實(shí)現(xiàn)單例.

枚舉與策略模式

特定的常量類型與主體中的方法或行為有關(guān)時(shí),即當(dāng)數(shù)據(jù)與行為之間有關(guān)聯(lián)時(shí),可以考慮使用枚舉來(lái)實(shí)現(xiàn)策略模式.

如我們需要實(shí)現(xiàn)加減運(yùn)算,就可以在枚舉類型中聲明一個(gè) apply抽象方法,在特定于常量的方法(Constant-specific class body的Constant -specific method implementation)中,用具體實(shí)現(xiàn)抽象方法.

public enum Operation {
    PLUS {
        // 實(shí)例中實(shí)現(xiàn)抽象方法
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        public double apply(double x, double y) {
            return x - y;
        }
    };

    // 聲明抽象方法
    public abstract double apply(double x, double y);
}
//調(diào)用
 double result = Operation.PLUS.apply(1, 2);

枚舉與Android

在舊版的Android開(kāi)發(fā)者官網(wǎng)的指南 Managing Your App's Memory,新版中已經(jīng)被移除.

有這么一句話 :

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

使用枚舉常量比使用final static來(lái)實(shí)現(xiàn)常量定義,枚舉的內(nèi)存消耗比后高不止兩倍. 你應(yīng)該嚴(yán)格避免在Android上使用枚舉.

導(dǎo)致很多開(kāi)發(fā)者把它當(dāng)成了教條,禁止在Android上使用枚舉.

從反編譯的Color類中可以發(fā)現(xiàn), 枚舉為每一個(gè)對(duì)象創(chuàng)建一個(gè)枚舉對(duì)象,枚舉對(duì)象里面至少有 一個(gè)String類型(name),和一個(gè)int類型(ordinal)再加上對(duì)象頭部占用的內(nèi)存.(此處還忽略了$VALUS數(shù)組的創(chuàng)建消耗).
單個(gè)枚舉類型常量,比static final聲明的常量占用的內(nèi)存大的多.

因此,不建議在Android中使用枚舉常量,而更偏向于使用 static final來(lái)定義常量.

但是,枚舉常量中有類型安全檢查的功能,使用常規(guī)的實(shí)現(xiàn),沒(méi)有這種功能.
這里我們可以使用android提供的注解來(lái)實(shí)現(xiàn)類型檢查. @StringDef@IntDef
具體可以參考這篇文章. Android Performance: Avoid using ENUM on Android

但是,一定不能使用枚舉嗎?

我覺(jué)得并不如此,當(dāng)數(shù)據(jù)和行為有關(guān)聯(lián)時(shí),或者說(shuō)數(shù)據(jù)受到行為的控制時(shí),可以考慮使用策略枚舉.

復(fù)雜的枚舉

EnumSet,EnumMap并不常用,這里不做過(guò)多解釋,想了解的可以參考 深入理解Java枚舉類型(enum)

引用

  1. Android Performance: Avoid using ENUM on Android
  2. 深入理解Java枚舉類型(enum)
  3. Java 枚舉會(huì)比靜態(tài)常量更消耗內(nèi)存嗎
  4. Should I strictly avoid using enums on Android?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容