1. 程式人生 > >深入理解Java列舉型別(enum)

深入理解Java列舉型別(enum)

關聯文章:

本篇主要是深入對Java中列舉型別進行分析,主要內容如下:

理解列舉型別

列舉型別是Java 5中新增特性的一部分,它是一種特殊的資料型別,之所以特殊是因為它既是一種類(class)型別卻又比類型別多了些特殊的約束,但是這些約束的存在也造就了列舉型別的簡潔性、安全性以及便捷性。下面先來看看什麼是列舉?如何定義列舉?

列舉的定義

回憶一下下面的程式,這是在沒有列舉型別時定義常量常見的方式

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 * 使用普通方式定義日期常量
 */
public class DayDemo { public static final int MONDAY =1; public static final int TUESDAY=2; public static final int WEDNESDAY=3; public static final int THURSDAY=4; public static final int FRIDAY=5; public static final int SATURDAY=6; public static final int SUNDAY=7
; }

上述的常量定義常量的方式稱為int列舉模式,這樣的定義方式並沒有什麼錯,但它存在許多不足,如在型別安全和使用方便性上並沒有多少好處,如果存在定義int值相同的變數,混淆的機率還是很大的,編譯器也不會提出任何警告,因此這種方式在枚舉出現後並不提倡,現在我們利用列舉型別來重新定義上述的常量,同時也感受一把列舉定義的方式,如下定義週一到週日的常量

//列舉型別,使用關鍵字enum
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

相當簡潔,在定義列舉型別時我們使用的關鍵字是enum,與class關鍵字類似,只不過前者是定義列舉型別,後者是定義類型別。列舉型別Day中分別定義了從週一到週日的值,這裡要注意,值一般是大寫的字母,多個值之間以逗號分隔。同時我們應該知道的是列舉型別可以像類(class)型別一樣,定義為一個單獨的檔案,當然也可以定義在其他類內部,更重要的是列舉常量在型別安全性和便捷性都很有保證,如果出現型別問題編譯器也會提示我們改進,但務必記住列舉表示的型別其取值是必須有限的,也就是說每個值都是可以枚舉出來的,比如上述描述的一週共有七天。那麼該如何使用呢?如下:

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class EnumDemo {

    public static void main(String[] args){
        //直接引用
        Day day =Day.MONDAY;
    }

}
//定義列舉型別
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

就像上述程式碼那樣,直接引用列舉的值即可,這便是列舉型別的最簡單模型。

列舉實現原理

我們大概瞭解了列舉型別的定義與簡單使用後,現在有必要來了解一下列舉型別的基本實現原理。實際上在使用關鍵字enum建立列舉型別並編譯後,編譯器會為我們生成一個相關的類,這個類繼承了Java API中的java.lang.Enum類,也就是說通過關鍵字enum建立列舉型別在編譯後事實上也是一個類型別而且該類繼承自java.lang.Enum類。下面我們編譯前面定義的EnumDemo.java並檢視生成的class檔案來驗證這個結論:

//檢視目錄下的java檔案
zejian@zejiandeMBP enumdemo$ ls
EnumDemo.java
//利用javac命令編譯EnumDemo.java
zejian@zejiandeMBP enumdemo$ javac EnumDemo.java 
//檢視生成的class檔案,注意有Day.class和EnumDemo.class 兩個
zejian@zejiandeMBP enumdemo$ ls
Day.class  EnumDemo.class  EnumDemo.java

利用javac編譯前面定義的EnumDemo.java檔案後分別生成了Day.class和EnumDemo.class檔案,而Day.class就是列舉型別,這也就驗證前面所說的使用關鍵字enum定義列舉型別並編譯後,編譯器會自動幫助我們生成一個與列舉相關的類。我們再來看看反編譯Day.class檔案:

//反編譯Day.class
final class Day extends Enum
{
    //編譯器為我們新增的靜態的values()方法
    public static Day[] values()
    {
        return (Day[])$VALUES.clone();
    }
    //編譯器為我們新增的靜態的valueOf()方法,注意間接呼叫了Enum也類的valueOf方法
    public static Day valueOf(String s)
    {
        return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
    }
    //私有建構函式
    private Day(String s, int i)
    {
        super(s, i);
    }
     //前面定義的7種列舉例項
    public static final Day MONDAY;
    public static final Day TUESDAY;
    public static final Day WEDNESDAY;
    public static final Day THURSDAY;
    public static final Day FRIDAY;
    public static final Day SATURDAY;
    public static final Day SUNDAY;
    private static final Day $VALUES[];

    static 
    {    
        //例項化列舉例項
        MONDAY = new Day("MONDAY", 0);
        TUESDAY = new Day("TUESDAY", 1);
        WEDNESDAY = new Day("WEDNESDAY", 2);
        THURSDAY = new Day("THURSDAY", 3);
        FRIDAY = new Day("FRIDAY", 4);
        SATURDAY = new Day("SATURDAY", 5);
        SUNDAY = new Day("SUNDAY", 6);
        $VALUES = (new Day[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
}

從反編譯的程式碼可以看出編譯器確實幫助我們生成了一個Day類(注意該類是final型別的,將無法被繼承)而且該類繼承自java.lang.Enum類,該類是一個抽象類(稍後我們會分析該類中的主要方法),除此之外,編譯器還幫助我們生成了7個Day型別的例項物件分別對應列舉中定義的7個日期,這也充分說明了我們前面使用關鍵字enum定義的Day型別中的每種日期列舉常量也是實實在在的Day例項物件,只不過代表的內容不一樣而已。注意編譯器還為我們生成了兩個靜態方法,分別是values()和 valueOf(),稍後會分析它們的用法,到此我們也就明白了,使用關鍵字enum定義的列舉型別,在編譯期後,也將轉換成為一個實實在在的類,而在該類中,會存在每個在列舉型別中定義好變數的對應例項物件,如上述的MONDAY列舉型別對應public static final Day MONDAY;,同時編譯器會為該類建立兩個方法,分別是values()和valueOf()。ok~,到此相信我們對列舉的實現原理也比較清晰,下面我們深入瞭解一下java.lang.Enum類以及values()和valueOf()的用途。

列舉的常見方法

Enum抽象類常見方法

Enum是所有 Java 語言列舉型別的公共基本類(注意Enum是抽象類),以下是它的常見方法:

返回型別 方法名稱 方法說明
int compareTo(E o) 比較此列舉與指定物件的順序
boolean equals(Object other) 當指定物件等於此列舉常量時,返回 true。
Class<?> getDeclaringClass() 返回與此列舉常量的列舉型別相對應的 Class 物件
String name() 返回此列舉常量的名稱,在其列舉宣告中對其進行宣告
int ordinal() 返回列舉常量的序數(它在列舉宣告中的位置,其中初始常量序數為零)
String toString() 返回列舉常量的名稱,它包含在宣告中
static<T extends Enum<T>> T static valueOf(Class<T> enumType, String name) 返回帶指定名稱的指定列舉型別的列舉常量。

這裡主要說明一下ordinal()方法,該方法獲取的是列舉變數在列舉類中宣告的順序,下標從0開始,如日期中的MONDAY在第一個位置,那麼MONDAY的ordinal值就是0,如果MONDAY的宣告位置發生變化,那麼ordinal方法獲取到的值也隨之變化,注意在大多數情況下我們都不應該首先使用該方法,畢竟它總是變幻莫測的。compareTo(E o)方法則是比較列舉的大小,注意其內部實現是根據每個列舉的ordinal值大小進行比較的。name()方法與toString()幾乎是等同的,都是輸出變數的字串形式。至於valueOf(Class<T> enumType, String name)方法則是根據列舉類的Class物件和列舉名稱獲取列舉常量,注意該方法是靜態的,後面在列舉單例時,我們還會詳細分析該方法,下面的程式碼演示了上述方法:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class EnumDemo {

    public static void main(String[] args){

        //建立列舉陣列
        Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY,
                Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY};

        for (int i = 0; i <days.length ; i++) {
            System.out.println("day["+i+"].ordinal():"+days[i].ordinal());
        }

        System.out.println("-------------------------------------");
        //通過compareTo方法比較,實際上其內部是通過ordinal()值比較的
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));

        //獲取該列舉物件的Class物件引用,當然也可以通過getClass方法
        Class<?> clazz = days[0].getDeclaringClass();
        System.out.println("clazz:"+clazz);

        System.out.println("-------------------------------------");

        //name()
        System.out.println("days[0].name():"+days[0].name());
        System.out.println("days[1].name():"+days[1].name());
        System.out.println("days[2].name():"+days[2].name());
        System.out.println("days[3].name():"+days[3].name());

        System.out.println("-------------------------------------");

        System.out.println("days[0].toString():"+days[0].toString());
        System.out.println("days[1].toString():"+days[1].toString());
        System.out.println("days[2].toString():"+days[2].toString());
        System.out.println("days[3].toString():"+days[3].toString());

        System.out.println("-------------------------------------");

        Day d=Enum.valueOf(Day.class,days[0].name());
        Day d2=Day.valueOf(Day.class,days[0].name());
        System.out.println("d:"+d);
        System.out.println("d2:"+d2);
    }
 /**
 執行結果:
   day[0].ordinal():0
   day[1].ordinal():1
   day[2].ordinal():2
   day[3].ordinal():3
   day[4].ordinal():4
   day[5].ordinal():5
   day[6].ordinal():6
   -------------------------------------
   days[0].compareTo(days[1]):-1
   days[0].compareTo(days[1]):-2
   clazz:class com.zejian.enumdemo.Day
   -------------------------------------
   days[0].name():MONDAY
   days[1].name():TUESDAY
   days[2].name():WEDNESDAY
   days[3].name():THURSDAY
   -------------------------------------
   days[0].toString():MONDAY
   days[1].toString():TUESDAY
   days[2].toString():WEDNESDAY
   days[3].toString():THURSDAY
   -------------------------------------
   d:MONDAY
   d2:MONDAY
   */

}
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

到此對於抽象類Enum類的基本內容就介紹完了,這裡提醒大家一點,Enum類內部會有一個建構函式,該建構函式只能有編譯器呼叫,我們是無法手動操作的,不妨看看Enum類的主要原始碼:

//實現了Comparable
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    private final String name; //列舉字串名稱

    public final String name() {
        return name;
    }

    private final int ordinal;//列舉順序值

    public final int ordinal() {
        return ordinal;
    }

    //列舉的構造方法,只能由編譯器呼叫
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public String toString() {
        return name;
    }

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

    //比較的是ordinal值
    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值比較大小
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        //獲取class物件引用,getClass()是Object的方法
        Class<?> clazz = getClass();
        //獲取父類Class物件引用
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }


    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        //enumType.enumConstantDirectory()獲取到的是一個map集合,key值就是name值,value則是列舉變數值   
        //enumConstantDirectory是class物件內部的方法,根據class物件獲取一個map集合的值       
        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);
    }

    //.....省略其他沒用的方法
}

通過Enum原始碼,可以知道,Enum實現了Comparable介面,這也是可以使用compareTo比較的原因,當然Enum建構函式也是存在的,該函式只能由編譯器呼叫,畢竟我們只能使用enum關鍵字定義列舉,其他事情就放心交給編譯器吧。

//由編譯器呼叫
protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

編譯器生成的Values方法與ValueOf方法

values()方法和valueOf(String name)方法是編譯器生成的static方法,因此從前面的分析中,在Enum類中並沒出現values()方法,但valueOf()方法還是有出現的,只不過編譯器生成的valueOf()方法需傳遞一個name引數,而Enum自帶的靜態方法valueOf()則需要傳遞兩個方法,從前面反編譯後的程式碼可以看出,編譯器生成的valueOf方法最終還是呼叫了Enum類的valueOf方法,下面通過程式碼來演示這兩個方法的作用:

Day[] days2 = Day.values();
System.out.println("day2:"+Arrays.toString(days2));
Day day = Day.valueOf("MONDAY");
System.out.println("day:"+day);

/**
 輸出結果:
 day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 day:MONDAY
 */

從結果可知道,values()方法的作用就是獲取列舉類中的所有變數,並作為陣列返回,而valueOf(String name)方法與Enum類中的valueOf方法的作用類似根據名稱獲取列舉變數,只不過編譯器生成的valueOf方法更簡潔些只需傳遞一個引數。這裡我們還必須注意到,由於values()方法是由編譯器插入到列舉類中的static方法,所以如果我們將列舉例項向上轉型為Enum,那麼values()方法將無法被呼叫,因為Enum類中並沒有values()方法,valueOf()方法也是同樣的道理,注意是一個引數的。

 //正常使用
Day[] ds=Day.values();
//向上轉型Enum
Enum e = Day.MONDAY;
//無法呼叫,沒有此方法
//e.values();

列舉與Class物件

上述我們提到當列舉例項向上轉型為Enum型別後,values()方法將會失效,也就無法一次性獲取所有列舉例項變數,但是由於Class物件的存在,即使不使用values()方法,還是有可能一次獲取到所有列舉例項變數的,在Class物件中存在如下方法:

返回型別 方法名稱 方法說明
T[] getEnumConstants() 返回該列舉型別的所有元素,如果Class物件不是列舉型別,則返回null。
boolean isEnum() 當且僅當該類宣告為原始碼中的列舉時返回 true

因此通過getEnumConstants()方法,同樣可以輕而易舉地獲取所有列舉例項變數下面通過程式碼來演示這個功能:

//正常使用
Day[] ds=Day.values();
//向上轉型Enum
Enum e = Day.MONDAY;
//無法呼叫,沒有此方法
//e.values();
//獲取class物件引用
Class<?> clasz = e.getDeclaringClass();
if(clasz.isEnum()) {
    Day[] dsz = (Day[]) clasz.getEnumConstants();
    System.out.println("dsz:"+Arrays.toString(dsz));
}

/**
   輸出結果:
   dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 */

正如上述程式碼所展示,通過Enum的class物件的getEnumConstants方法,我們仍能一次性獲取所有的列舉例項常量。

列舉的進階用法

在前面的分析中,我們都是基於簡單列舉型別的定義,也就是在定義列舉時只定義了列舉例項型別,並沒定義方法或者成員變數,實際上使用關鍵字enum定義的列舉類,除了不能使用繼承(因為編譯器會自動為我們繼承Enum抽象類而Java只支援單繼承,因此列舉類是無法手動實現繼承的),可以把enum類當成常規類,也就是說我們可以向enum類中新增方法和變數,甚至是mian方法,下面就來感受一把。

向enum類新增方法與自定義建構函式

重新定義一個日期列舉類,帶有desc成員變數描述該日期的對於中文描述,同時定義一個getDesc方法,返回中文描述內容,自定義私有建構函式,在宣告列舉例項時傳入對應的中文描述,程式碼如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public enum Day2 {
    MONDAY("星期一"),
    TUESDAY("星期二"),
    WEDNESDAY("星期三"),
    THURSDAY("星期四"),
    FRIDAY("星期五"),
    SATURDAY("星期六"),
    SUNDAY("星期日");//記住要用分號結束

    private String desc;//中文描述

    /**
     * 私有構造,防止被外部呼叫
     * @param desc
     */
    private Day2(String desc){
        this.desc=desc;
    }

    /**
     * 定義方法,返回描述,跟常規類的定義沒區別
     * @return
     */
    public String getDesc(){
        return desc;
    }

    public static void main(String[] args){
        for (Day2 day:Day2.values()) {
            System.out.println("name:"+day.name()+
                    ",desc:"+day.getDesc());
        }
    }

    /**
     輸出結果:
     name:MONDAY,desc:星期一
     name:TUESDAY,desc:星期二
     name:WEDNESDAY,desc:星期三
     name:THURSDAY,desc:星期四
     name:FRIDAY,desc:星期五
     name:SATURDAY,desc:星期六
     name:SUNDAY,desc:星期日
     */
}

從上述程式碼可知,在enum類中確實可以像定義常規類一樣宣告變數或者成員方法。但是我們必須注意到,如果打算在enum類中定義方法,務必在宣告完列舉例項後使用分號分開,倘若在列舉例項前定義任何方法,編譯器都將會報錯,無法編譯通過,同時即使自定義了建構函式且enum的定義結束,我們也永遠無法手動呼叫建構函式建立列舉例項,畢竟這事只能由編譯器執行。

關於覆蓋enum類方法

既然enum類跟常規類的定義沒什麼區別(實際上enum還是有些約束的),那麼覆蓋父類的方法也不會是什麼難說,可惜的是父類Enum中的定義的方法只有toString方法沒有使用final修飾,因此只能覆蓋toString方法,如下通過覆蓋toString省去了getDesc方法:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public enum Day2 {
    MONDAY("星期一"),
    TUESDAY("星期二"),
    WEDNESDAY("星期三"),
    THURSDAY("星期四"),
    FRIDAY("星期五"),
    SATURDAY("星期六"),
    SUNDAY("星期日");//記住要用分號結束

    private String desc;//中文描述

    /**
     * 私有構造,防止被外部呼叫
     * @param desc
     */
    private Day2(String desc){
        this.desc=desc;
    }

    /**
     * 覆蓋
     * @return
     */
    @Override
    public String toString() {
        return desc;
    }


    public static void main(String[] args){
        for (Day2 day:Day2.values()) {
            System.out.println("name:"+day.name()+
                    ",desc:"+day.toString());
        }
    }

    /**
     輸出結果:
     name:MONDAY,desc:星期一
     name:TUESDAY,desc:星期二
     name:WEDNESDAY,desc:星期三
     name:THURSDAY,desc:星期四
     name:FRIDAY,desc:星期五
     name:SATURDAY,desc:星期六
     name:SUNDAY,desc:星期日
     */
}

enum類中定義抽象方法

與常規抽象類一樣,enum類允許我們為其定義抽象方法,然後使每個列舉例項都實現該方法,以便產生不同的行為方式,注意abstract關鍵字對於列舉類來說並不是必須的如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/9.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public enum EnumDemo3 {

    FIRST{
        @Override
        public String getInfo() {
            return "FIRST TIME";
        }
    },
    SECOND{
        @Override
        public String getInfo() {
            return "SECOND TIME";
        }
    }

    ;

    /**
     * 定義抽象方法
     * @return
     */
    public abstract String getInfo();

    //測試
    public static void main(String[] args){
        System.out.println("F:"+EnumDemo3.FIRST.getInfo());
        System.out.println("S:"+EnumDemo3.SECOND.getInfo());
        /**
         輸出結果:
         F:FIRST TIME
         S:SECOND TIME
         */
    }
}

通過這種方式就可以輕而易舉地定義每個列舉例項的不同行為方式。我們可能注意到,enum類的例項似乎表現出了多型的特性,可惜的是列舉型別的例項終究不能作為型別傳遞使用,就像下面的使用方式,編譯器是不可能答應的:

//無法通過編譯,畢竟EnumDemo3.FIRST是個例項物件
 public void text(EnumDemo3.FIRST instance){ }

在列舉例項常量中定義抽象方法

enum類與介面

由於Java單繼承的原因,enum類並不能再繼承其它類,但並不妨礙它實現介面,因此enum類同樣是可以實現多介面的,如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */

interface food{
    void eat();
}

interface sport{
    void run();
}

public enum EnumDemo2 implements food ,sport{
    FOOD,
    SPORT,
    ; //分號分隔

    @Override
    public void eat() {
        System.out.println("eat.....");
    }

    @Override
    public void run() {
        System.out.println("run.....");
    }
}

有時候,我們可能需要對一組資料進行分類,比如進行食物選單分類而且希望這些選單都屬於food型別,appetizer(開胃菜)、mainCourse(主菜)、dessert(點心)、Coffee等,每種分類下有多種具體的菜式或食品,此時可以利用介面來組織,如下(程式碼引用自Thinking in Java):

public interface Food {
  enum Appetizer implements Food {
    SALAD, SOUP, SPRING_ROLLS;
  }
  enum MainCourse implements Food {
    LASAGNE, BURRITO, PAD_THAI,
    LENTILS, HUMMOUS, VINDALOO;
  }
  enum Dessert implements Food {
    TIRAMISU, GELATO, BLACK_FOREST_CAKE,
    FRUIT, CREME_CARAMEL;
  }
  enum Coffee implements Food {
    BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
    LATTE, CAPPUCCINO, TEA, HERB_TEA;
  }
}

public class TypeOfFood {
  public static void main(String[] args) {
    Food food = Appetizer.SALAD;
    food = MainCourse.LASAGNE;
    food = Dessert.GELATO;
    food = Coffee.CAPPUCCINO;
  }
} 

通過這種方式可以很方便組織上述的情景,同時確保每種具體型別的食物也屬於Food,現在我們利用一個列舉巢狀列舉的方式,把前面定義的菜譜存放到一個Meal選單中,通過這種方式就可以統一管理選單的資料了。

public enum Meal{
  APPETIZER(Food.Appetizer.class),
  MAINCOURSE(Food.MainCourse.class),
  DESSERT(Food.Dessert.class),
  COFFEE(Food.Coffee.class);
  private Food[] values;
  private Meal(Class<? extends Food> kind) {
    //通過class物件獲取列舉例項
    values = kind.getEnumConstants();
  }
  public interface Food {
    enum Appetizer implements Food {
      SALAD, SOUP, SPRING_ROLLS;
    }
    enum MainCourse implements Food {
      LASAGNE, BURRITO, PAD_THAI,
      LENTILS, HUMMOUS, VINDALOO;
    }
    enum Dessert implements Food {
      TIRAMISU, GELATO, BLACK_FOREST_CAKE,
      FRUIT, CREME_CARAMEL;
    }
    enum Coffee implements Food {
      BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
      LATTE, CAPPUCCINO, TEA, HERB_TEA;
    }
  }
} 

列舉與switch

關於列舉與switch是個比較簡單的話題,使用switch進行條件判斷時,條件引數一般只能是整型,字元型。而列舉型確實也被switch所支援,在java 1.7後switch也對字串進行了支援。這裡我們簡單看一下switch與列舉型別的使用:


/**
 * Created by zejian on 2017/5/9.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */

enum Color {GREEN,RED,BLUE}

public class EnumDemo4 {

    public static void printName(Color color){
        switch (color){
            case BLUE: //無需使用Color進行引用
                System.out.println("藍色");
                break;
            case RED:
                System.out.println("紅色");
                break;
            case GREEN:
                System.out.println("綠色");
                break;
        }
    }

    public static void main(String[] args){
        printName(Color.BLUE);
        printName(Color.RED);
        printName(Color.GREEN);

        //藍色
        //紅色
        //綠色
    }
}

需要注意的是使用在於switch條件進行結合使用時,無需使用Color引用。

列舉與單例模式

單例模式可以說是最常使用的設計模式了,它的作用是確保某個類只有一個例項,自行例項化並向整個系統提供這個例項。在實際應用中,執行緒池、快取、日誌物件、對話方塊物件常被設計成單例,總之,選擇單例模式就是為了避免不一致狀態,下面我們將會簡單說明單例模式的幾種主要編寫方式,從而對比出使用列舉實現單例模式的優點。首先看看餓漢式的單例模式:

/**
 * Created by wuzejian on 2017/5/9.
 * 餓漢式(基於classloder機制避免了多執行緒的同步問題)
 */
public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry() {
    }

    public static SingletonHungry getInstance() {
        return instance;
    }
}

顯然這種寫法比較簡單,但問題是無法做到延遲建立物件,事實上如果該單例類涉及資源較多,建立比較耗時間時,我們更希望它可以儘可能地延遲載入,從而減小初始化的負載,於是便有了如下的懶漢式單例:

/**
 * Created by wuzejian on 2017/5/9..
 * 懶漢式單例模式(適合多執行緒安全)
 */
public class SingletonLazy {

    private static volatile SingletonLazy instance;

    private SingletonLazy() {
    }

    public static synchronized SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

這種寫法能夠在多執行緒中很好的工作避免同步問題,同時也具備lazy loading機制,遺憾的是,由於synchronized的存在,效率很低,在單執行緒的情景下,完全可以去掉synchronized,為了兼顧效率與效能問題,改進後代碼如下:

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

這種編寫方式被稱為“雙重檢查鎖”,主要在getSingleton()方法中,進行兩次null檢查。這樣可以極大提升併發度,進而提升效能。畢竟在單例中new的情況非常少,絕大多數都是可以並行的讀操作,因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,也就提高了執行效率。但是必須注意的是volatile關鍵字,該關鍵字有兩層語義。第一層語義是可見性,可見性是指在一個執行緒中對該變數的修改會馬上由工作記憶體(Work Memory)寫回主記憶體(Main Memory),所以其它執行緒會馬上讀取到已修改的值,關於工作記憶體和主記憶體可簡單理解為快取記憶體(直接與CPU打交道)和主存(日常所說的記憶體條),注意工作記憶體是執行緒獨享的,主存是執行緒共享的。volatile的第二層語義是禁止指令重排序優化,我們寫的程式碼(特別是多執行緒程式碼),由於編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程式執行結果與原始碼相同,卻不保證實際指令的順序與原始碼相同,這在單執行緒並沒什麼問題,然而一旦引入多執行緒環境,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題,值得關注的是volatile的禁止指令重排序優化功能在Java 1.5後才得以實現,因此1.5前的版本仍然是不安全的,即使使用了volatile關鍵字。或許我們可以利用靜態內部類來實現更安全的機制,靜態內部類單例模式如下:

/**
 * Created by wuzejian on 2017/5/9.
 * 靜態內部類
 */
public class SingletonInner {
    private static class Holder {
        private static SingletonInner singleton = new SingletonInner();
    }

    private SingletonInner(){}

    public static SingletonInner getSingleton(){
        return Holder.singleton;
    }
}

正如上述程式碼所展示的,我們把Singleton例項放到一個靜態內部類中,這樣可以避免了靜態例項在Singleton類的載入階段(類載入過程的其中一個階段的,此時只建立了Class物件,關於Class物件可以看博主另外一篇博文, 深入理解Java型別資訊(Class物件)與反射機制)就建立物件,畢竟靜態變數初始化是在SingletonInner類初始化時觸發的,並且由於靜態內部類只會被載入一次,所以這種寫法也是執行緒安全的。從上述4種單例模式的寫法中,似乎也解決了效率與懶載入的問題,但是它們都有兩個共同的缺點:

  • 序列化可能會破壞單例模式,比較每次反序列化一個序列化的物件例項時都會建立一個新的例項,解決方案如下:

    //測試例子(四種寫解決方式雷同)
    public class Singleton implements java.io.Serializable {     
       public static Singleton INSTANCE = new Singleton();     
    
       protected Singleton() {     
       }  
    
       //反序列時直接返回當前INSTANCE
       private Object readResolve() {     
                return INSTANCE;     
          }    
    }   
  • 使用反射強行呼叫私有構造器,解決方式可以修改構造器,讓它在建立第二個例項的時候拋異常,如下:

    public static Singleton INSTANCE = new Singleton();     
    private static volatile  boolean  flag = true;
    private Singleton(){
        if(flag){
        flag = false;   
        }else{
            throw new RuntimeException("The instance  already exists !");
        }
    }

如上所述,問題確實也得到了解決,但問題是我們為此付出了不少努力,即添加了不少程式碼,還應該注意到如果單例類維持了其他物件的狀態時還需要使他們成為transient的物件,這種就更復雜了,那有沒有更簡單更高效的呢?當然是有的,那就是列舉單例了,先來看看如何實現:

/**
 * Created by wuzejian on 2017/5/9.
 * 列舉單利
 */
public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

程式碼相當簡潔,我們也可以像常規類一樣編寫enum類,為其新增變數和方法,訪問方式也更簡單,使用SingletonEnum.INSTANCE進行訪問,這樣也就避免呼叫getInstance方法,更重要的是使用列舉單例的寫法,我們完全不用考慮序列化和反射的問題。列舉序列化是由jvm保證的,每一個列舉型別和定義的列舉變數在JVM中都是唯一的,在列舉型別的序列化和反序列化上,Java做了特殊的規定:在序列化時Java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查詢列舉物件。同時,編譯器是不允許任何對這種序列化機制的定製的並禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,從而保證了列舉例項的唯一性,這裡我們不妨再次看看Enum類的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 constant " + enumType.getCanonicalName() + "." + name);
  }

實際上通過呼叫enumType(Class物件的引用)的enumConstantDirectory方法獲取到的是一個Map集合,在該集合中存放了以列舉name為key和以列舉例項變數為value的Key&Value資料,因此通過name的值就可以獲取到列舉例項,看看enumConstantDirectory方法原始碼:

Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            //getEnumConstantsShared最終通過反射呼叫列舉類的values方法
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            //map存放了當前enum類的所有列舉例項變數,以name為key值
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;

到這裡我們也就可以看出列舉序列化確實不會重新建立新例項,jvm保證了每個列舉例項變數的唯一性。再來看看反射到底能不能建立列舉,下面試圖通過反射獲取構造器並建立列舉

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  //獲取列舉類的建構函式(前面的原始碼已分析過)
   Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //建立列舉
   SingletonEnum singleton=constructor.newInstance("otherInstance",9);
  }

執行報錯

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

顯然告訴我們不能使用反射建立列舉類,這是為什麼呢?不妨看看newInstance方法原始碼:

 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //這裡判斷Modifier.ENUM是不是列舉修飾符,如果是就拋異常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

原始碼很瞭然,確實無法使用反射建立列舉例項,也就是說明了建立列舉例項只有編譯器能夠做到而已。顯然列舉單例模式確實是很不錯的選擇,因此我們推薦使用它。但是這總不是萬能的,對於android平臺這個可能未必是最好的選擇,在android開發中,記憶體優化是個大塊頭,而使用列舉時佔用的記憶體常常是靜態變數的兩倍還多,因此android官方在記憶體優化方面給出的建議是儘量避免在android中使用enum。但是不管如何,關於單例,我們總是應該記住:執行緒安全,延遲載入,序列化與反序列化安全,反射安全是很重重要的。

EnumMap

EnumMap基本用法

先思考這樣一個問題,現在我們有一堆size大小相同而顏色不同的資料,需要統計出每種顏色的數量是多少以便將資料錄入倉庫,定義如下列舉用於表示顏色Color:

enum Color {
    GREEN,RED,BLUE,YELLOW
}

我們有如下解決方案,使用Map集合來統計,key值作為顏色名稱,value代表衣服數量,如下:

package com.zejian.enumdemo;

import