走進 JDK 之 Enum

什麼是枚舉

什麼是枚舉?說實話,在我這些年的開發生涯中,用過枚舉的次數大概兩隻手均可以數的過來。固然你不能說枚舉一無可取,只能說是我對 Java 理解的還不夠深入,在可使用枚舉的時候並無去使用。html

假設我有兩個孩子(其實不用假設),每到週末他們都不知道去上什麼輔導班。因而我就寫了一個簡單的程序來告訴他們,僞代碼以下:java

public final static int DAVID = 0;
public final static int MARRY = 1;

public void order(int who){
    switch (who){
        case DAVID:
            System.out.println("大衛,去練武術!");
            break;
        case MARRY:
            System.out.println("瑪麗,去練跳舞!");
            break;
         default:
            System.out.println("今天休息");
    }
}
複製代碼

而後我告訴 David 你輸入 0,告訴 Marry 你輸入 1,就能夠了。不久以後,輔導班老師就指點問候我了,您家的兩個孩子呢?這個氣的我呀,立馬回家看了看日誌,兩個孩子除了 0 和 1,其餘數字都輸齊了。編程

因而可知,這樣直接使用 int 常量沒法限定用戶的輸入,你讓它輸 0 或 1,它恰恰輸個 45678。從代碼可讀性來講,參數是個 int 值,並非那麼直觀的就能夠看出來應該輸入什麼。無奈之下,我只得掏出 《Java 編程思想》,來治治這兩個熊孩子。下面是我優化的程序:數組

public enum Child { DAVID, MARY }

public void order(Child who) {
    switch (who) {
        case DAVID:
            System.out.println("大衛,去練武術!");
            break;
        case MARY:
            System.out.println("瑪麗,去練跳舞!");
            break;
        default:
            System.out.println("今天休息");
    }
}
複製代碼

實際上已經能夠刪除 default 了,由於參數是枚舉類 Child,輸入範圍已經被限定爲我定義的枚舉,輸入其餘值將沒法經過編譯。兩個熊孩子終於能夠愉快的去上課了。安全

枚舉是 Java 1.5 中新增的引用類型,指由一組固定的常量組成合法值的類型,其實上面的例子並不那麼適合枚舉,例如一年四季,一週七天這樣的,更加適合枚舉。相比使用 int 常量來定義,枚舉具備類型安全和可讀性良好的優點。《Effective Java》中也鼓勵 用 enum 代替 int 常量bash

除此以外,還能夠給枚舉添加字段和方法,例如,我想給每一個孩子加上姓名,能夠這麼作:微信

public enum Child {

    DAVID("David"), MARY("Marry");

    private String name;

    Child(String name){
        this.name=name;
    }
    
    public static void main(String[] args){
        for (Child child:Child.values()){
            System.out.println(child.ordinal()+" "+child.name);
        }
    }
}
複製代碼

注意,枚舉的定義只能放在第一行,若是你把 DAVID("David"), MARY("Marry"); 放在其餘位置,是沒法經過編譯的。另外別忘了,最後一個枚舉常量後面要加分號。函數

values() 會枚舉出 Child 中定義的全部枚舉常量。打印結果以下:性能

0 David
1 Marry
複製代碼

是否是比以前的 int 常量那種方式強大多了。優化

源碼解析

Enum

走進 JDK 系列,那必然是少不了源碼解析的。

類定義

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {}
複製代碼

Enum 是一個抽象類,咱們本身定義的枚舉類都繼承了 Enum。實現了 Comparable 接口,重寫了 compare 的邏輯。實現了 Serializable 接口,能夠序列化。可是枚舉對序列化做了必定的限制,在序列化的時候僅僅是將枚舉對象的 name 屬性輸出到結果中,反序列化的時候則是經過 Enum.valueOf() 方法來查找枚舉對象。同時,編譯器是不容許任何對這種序列化機制的定製的,所以禁用了 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法。所以,用枚舉來實現單例模式的話,是反序列化安全的,由於即便反序列化也不會生成新的對象。

字段

private final String name; // 枚舉實例的名稱,就是枚舉聲明中的名稱
private final int ordinal; // 在枚舉聲明中的次序,從 0 開始
複製代碼

枚舉類就只有這兩個字段。name 就是枚舉聲明的名字,好比 DAVIDname 就是 DAVIDordinal 就是聲明中的次序,之因此在 switch 中可使用枚舉,就是由於編譯器會自動調用枚舉的 ordinal() 方法。

構造函數

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}
複製代碼

受保護的,你不能調用枚舉類的構造函數。

方法

public final String name() {
    return name; // 返回 name
}

public final int ordinal() {
    return ordinal; // 返回 ordinal
}

public String toString() {
    return name; // 能夠重寫使得枚舉類返回一個用戶友好的名字,默認返回 name
}

public final boolean equals(Object other) {
    return this==other; // 枚舉的 equals 和 == 是等價的
}

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException(); // 枚舉不支持 clone,單例
}

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
}

// 根據指定的枚舉類型和名稱返回枚舉實例,在反序列化中會使用
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);
}
複製代碼

還記得以前使用過的 values() 方法嗎,用來遍歷枚舉的。找遍了 Enum.java 也沒有看到這個方法,既然父類中沒有這個方法,那麼必定是在子類中聲明的了。下面咱們來驗證一下。

Enum 子類

咱們是怎麼定義枚舉的,

public enum Child {
    DAVID("David"), MARY("Marry");
    private String name;
    Child(String name){
        this.name=name;
    }
}
複製代碼

並無顯示的去繼承 Enum,而是使用了 enum 關鍵字,雖然沒有使用 class 關鍵字,但其實它仍是一個類,只是編譯器幫咱們作了中間步驟。以前有說過,知其然不知其因此然的時候,javap 是你的好幫手。此次咱們不 javap 了,畢竟字節碼可讀性不是那麼的好。咱們使用 jad 反編譯 Child.class 文件,獲得結果以下:

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name: Child.java

package enums;

public final class Child extends Enum {

    public static Child[] values() {
        return (Child[])$VALUES.clone();
    }

    public static Child valueOf(String s) {
        return (Child)Enum.valueOf(enums/Child, s);
    }

    private Child(String s, int i, String s1) {
        super(s, i);
        name = s1;
    }

    public static final Child DAVID;
    public static final Child MARY;
    private String name;
    private static final Child $VALUES[];

    static {
        DAVID = new Child("DAVID", 0, "David");
        MARY = new Child("MARY", 1, "Marry");
        $VALUES = (new Child[] {
            DAVID, MARY
        });
    }
}
複製代碼

看到這個東西,你就應該全明白了。提及來叫枚舉,其實就是普通的類,只是咱們只須要使用 enum 關鍵字,編譯器就會幫咱們作好一切。枚舉中聲明的變量都是 static final 的,且在 static 代碼塊中進行初始化,並存入對象數組 $VALUES。因此枚舉實例的建立默認是線程安全的。除此以外,編譯器還自動生成了 values() 方法,返回 $VALUES 數組的克隆。

說到這裏,你應該對枚舉的原理很清楚了。枚舉的種種特性都特別契合單例模式,天生的線程安全和反序列化安全,這都是其餘單例模式所不具有的。可是在我所見過的代碼中,真正使用枚舉去作單例的好像少之又少。具體的緣由有待考究。

真的要使用枚舉嗎?

站在 Android 開發者的角度,實際上官方是不建議咱們使用枚舉的。

枚舉佔用的空間一般是靜態常量的兩倍。你應該嚴格避免在 Android 中使用枚舉。

其實我並非徹底贊同。MVP 多了那麼多接口和類,咱們應該使用嗎?在現在的手機內存下,若是你的應用發生了 OOM,我想枚舉應該不是罪魁禍首吧。只要不過分使用,站在加強代碼可讀性,保證類型安全的角度,用枚舉代替靜態常量確定是個好選擇。固然若是你說你要追求極致的性能,那倒沒必要使用枚舉了。

文章首發微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索