深入理解Java枚舉類型(enum)

【版權申明】未經博主同意,謝絕轉載!(請尊重原創,博主保留追究權)
http://blog.csdn.net/javazejian/article/details/71333103
出自【zejian的博客】

關聯文章:

深入理解Java類型信息(Class對象)與反射機制

深入理解Java枚舉類型(enum)

深入理解Java註解類型(@Annotation)

深入理解Java併發之synchronized實現原理

深入理解Java內存模型(JMM)及volatile關鍵字

深入理解Java類加載器(ClassLoader)

本篇主要是深入對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 java.util.*;

/**
 * Created by zejian on 2017/5/10.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class EnumMapDemo {
    public static void main(String[] args){
        List<Clothes> list = new ArrayList<>();
        list.add(new Clothes("C001",Color.BLUE));
        list.add(new Clothes("C002",Color.YELLOW));
        list.add(new Clothes("C003",Color.RED));
        list.add(new Clothes("C004",Color.GREEN));
        list.add(new Clothes("C005",Color.BLUE));
        list.add(new Clothes("C006",Color.BLUE));
        list.add(new Clothes("C007",Color.RED));
        list.add(new Clothes("C008",Color.YELLOW));
        list.add(new Clothes("C009",Color.YELLOW));
        list.add(new Clothes("C010",Color.GREEN));
        //方案1:使用HashMap
        Map<String,Integer> map = new HashMap<>();
        for (Clothes clothes:list){
           String colorName=clothes.getColor().name();
           Integer count = map.get(colorName);
            if(count!=null){
                map.put(colorName,count+1);
            }else {
                map.put(colorName,1);
            }
        }

        System.out.println(map.toString());

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

        //方案2:使用EnumMap
        Map<Color,Integer> enumMap=new EnumMap<>(Color.class);

        for (Clothes clothes:list){
            Color color=clothes.getColor();
            Integer count = enumMap.get(color);
            if(count!=null){
                enumMap.put(color,count+1);
            }else {
                enumMap.put(color,1);
            }
        }

        System.out.println(enumMap.toString());
    }

    /**
     輸出結果:
     {RED=2, BLUE=3, YELLOW=3, GREEN=2}
     ---------------
     {GREEN=2, RED=2, BLUE=3, YELLOW=3}
     */
}

代碼比較簡單,我們使用兩種解決方案,一種是HashMap,一種EnumMap,雖然都統計出了正確的結果,但是EnumMap作爲枚舉的專屬的集合,我們沒有理由再去使用HashMap,畢竟EnumMap要求其Key必須爲Enum類型,因而使用Color枚舉實例作爲key是最恰當不過了,也避免了獲取name的步驟,更重要的是EnumMap效率更高,因爲其內部是通過數組實現的(稍後分析),注意EnumMap的key值不能爲null,雖說是枚舉專屬集合,但其操作與一般的Map差不多,概括性來說EnumMap是專門爲枚舉類型量身定做的Map實現,雖然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap會更加高效,它只能接收同一枚舉類型的實例作爲鍵值且不能爲null,由於枚舉類型實例的數量相對固定並且有限,所以EnumMap使用數組來存放與枚舉類型對應的值,畢竟數組是一段連續的內存空間,根據程序局部性原理,效率會相當高。下面我們來進一步瞭解EnumMap的用法,先看構造函數:

//創建一個具有指定鍵類型的空枚舉映射。
EnumMap(Class<K> keyType) 
//創建一個其鍵類型與指定枚舉映射相同的枚舉映射,最初包含相同的映射關係(如果有的話)。     
EnumMap(EnumMap<K,? extends V> m) 
//創建一個枚舉映射,從指定映射對其初始化。
EnumMap(Map<K,? extends V> m)       

與HashMap不同,它需要傳遞一個類型信息,即Class對象,通過這個參數EnumMap就可以根據類型信息初始化其內部數據結構,另外兩隻是初始化時傳入一個Map集合,代碼演示如下:

//使用第一種構造
Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
//使用第二種構造
Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
//使用第三種構造
Map<Color,Integer> hashMap = new HashMap<>();
hashMap.put(Color.GREEN, 2);
hashMap.put(Color.BLUE, 3);
Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

至於EnumMap的方法,跟普通的map幾乎沒有區別,注意與HashMap的主要不同在於構造方法需要傳遞類型參數和EnumMap保證Key順序與枚舉中的順序一致,但請記住Key不能爲null。

EnumMap實現原理剖析

EnumMap的源碼有700多行,這裏我們主要分析其內部存儲結構,添加查找的實現,瞭解這幾點,對應EnumMap內部實現原理也就比較清晰了,先看數據結構和構造函數

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    //Class對象引用
    private final Class<K> keyType;

    //存儲Key值的數組
    private transient K[] keyUniverse;

    //存儲Value值的數組
    private transient Object[] vals;

    //map的size
    private transient int size = 0;

    //空map
    private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];

    //構造函數
    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }

}

EnumMap繼承了AbstractMap類,因此EnumMap具備一般map的使用方法,keyType表示類型信息,keyUniverse表示鍵數組,存儲的是所有可能的枚舉值,vals數組表示鍵對應的值,size表示鍵值對個數。在構造函數中通過keyUniverse = getKeyUniverse(keyType);初始化了keyUniverse數組的值,內部存儲的是所有可能的枚舉值,接着初始化了存在Value值得數組vals,其大小與枚舉實例的個數相同,getKeyUniverse方法實現如下

//返回枚舉數組
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
        //最終調用到枚舉類型的values方法,values方法返回所有可能的枚舉值
        return SharedSecrets.getJavaLangAccess()
                                        .getEnumConstantsShared(keyType);
    }

從方法的返回值來看,返回類型是枚舉數組,事實也是如此,最終返回值正是枚舉類型的values方法的返回值,前面我們分析過values方法返回所有可能的枚舉值,因此keyUniverse數組存儲就是枚舉類型的所有可能的枚舉值。接着看put方法的實現

 public V put(K key, V value) {
        typeCheck(key);//檢測key的類型
        //獲取存放value值得數組下標
        int index = key.ordinal();
        //獲取舊值
        Object oldValue = vals[index];
        //設置value值
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);//返回舊值
    }

這裏通過typeCheck方法進行了key類型檢測,判斷是否爲枚舉類型,如果類型不對,會拋出異常

private void typeCheck(K key) {
   Class<?> keyClass = key.getClass();//獲取類型信息
   if (keyClass != keyType && keyClass.getSuperclass() != keyType)
       throw new ClassCastException(keyClass + " != " + keyType);
}

接着通過int index = key.ordinal()的方式獲取到該枚舉實例的順序值,利用此值作爲下標,把值存儲在vals數組對應下標的元素中即vals[index],這也是爲什麼EnumMap能維持與枚舉實例相同存儲順序的原因,我們發現在對vals[]中元素進行賦值和返回舊值時分別調用了maskNull方法和unmaskNull方法

 //代表NULL值得空對象實例
  private static final Object NULL = new Object() {
        public int hashCode() {
            return 0;
        }

        public String toString() {
            return "java.util.EnumMap.NULL";
        }
    };

    private Object maskNull(Object value) {
        //如果值爲空,返回NULL對象,否則返回value
        return (value == null ? NULL : value);
    }

    @SuppressWarnings("unchecked")
    private V unmaskNull(Object value) {
        //將NULL對象轉換爲null值
        return (V)(value == NULL ? null : value);
    }

由此看來EnumMap還是允許存放null值的,但key絕對不能爲null,對於null值,EnumMap進行了特殊處理,將其包裝爲NULL對象,畢竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用於null的包裝和解包裝的。這就是EnumMap集合的添加過程。下面接着看獲取方法

 public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
    }

 //對Key值的有效性和類型信息進行判斷
 private boolean isValidKey(Object key) {
      if (key == null)
          return false;

      // Cheaper than instanceof Enum followed by getDeclaringClass
      Class<?> keyClass = key.getClass();
      return keyClass == keyType || keyClass.getSuperclass() == keyType;
  }

相對應put方法,get方法顯示相當簡潔,key有效的話,直接通過ordinal方法取索引,然後在值數組vals裏通過索引獲取值返回。remove方法如下:

 public V remove(Object key) {
        //判斷key值是否有效
        if (!isValidKey(key))
            return null;
        //直接獲取索引
        int index = ((Enum<?>)key).ordinal();

        Object oldValue = vals[index];
        //對應下標元素值設置爲null
        vals[index] = null;
        if (oldValue != null)
            size--;//減size
        return unmaskNull(oldValue);
    }

非常簡單,key值有效,通過key獲取下標索引值,把vals[]對應下標值設置爲null,size減一。查看是否包含某個值,

判斷是否包含某value
public boolean containsValue(Object value) {
    value = maskNull(value);
    //遍歷數組實現
    for (Object val : vals)
        if (value.equals(val))
            return true;

    return false;
}
//判斷是否包含key
public boolean containsKey(Object key) {
    return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}

判斷value直接通過遍歷數組實現,而判斷key就更簡單了,判斷key是否有效和對應vals[]中是否存在該值。ok~,這就是EnumMap的主要實現原理,即內部有兩個數組,長度相同,一個表示所有可能的鍵(枚舉值),一個表示對應的值,不允許keynull,但允許value爲null,鍵都有一個對應的索引,根據索引直接訪問和操作其鍵數組和值數組,由於操作都是數組,因此效率很高。

EnumSet

EnumSet是與枚舉類型一起使用的專用 Set 集合,EnumSet 中所有元素都必須是枚舉類型。與其他Set接口的實現類HashSet/TreeSet(內部都是用對應的HashMap/TreeMap實現的)不同的是,EnumSet在內部實現是位向量(稍後分析),它是一種極爲高效的位運算操作,由於直接存儲和操作都是bit,因此EnumSet空間和時間性能都十分可觀,足以媲美傳統上基於 int 的“位標誌”的運算,重要的是我們可像操作set集合一般來操作位運算,這樣使用代碼更簡單易懂同時又具備類型安全的優勢。注意EnumSet不允許使用 null 元素。試圖插入 null 元素將拋出 NullPointerException,但試圖測試判斷是否存在null 元素或移除 null 元素則不會拋出異常,與大多數collection 實現一樣,EnumSet不是線程安全的,因此在多線程環境下應該注意數據同步問題,ok~,下面先來簡單看看EnumSet的使用方式。

EnumSet用法

創建EnumSet並不能使用new關鍵字,因爲它是個抽象類,而應該使用其提供的靜態工廠方法,EnumSet的靜態工廠方法比較多,如下:

創建一個具有指定元素類型的空EnumSet。
EnumSet<E>  noneOf(Class<E> elementType)       
//創建一個指定元素類型幷包含所有枚舉值的EnumSet
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 創建一個包括枚舉值中指定範圍元素的EnumSet
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的補集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 創建一個包括參數中所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//創建一個包含參數容器中的所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

代碼演示如下:

package zejian;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

/**
 * Created by wuzejian on 2017/5/12.
 *
 */
enum Color {
    GREEN , RED , BLUE , BLACK , YELLOW
}


public class EnumSetDemo {

    public static void main(String[] args){

        //空集合
        EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
        System.out.println("添加前:"+enumSet.toString());
        enumSet.add(Color.GREEN);
        enumSet.add(Color.RED);
        enumSet.add(Color.BLACK);
        enumSet.add(Color.BLUE);
        enumSet.add(Color.YELLOW);
        System.out.println("添加後:"+enumSet.toString());

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

        //使用allOf創建包含所有枚舉類型的enumSet,其內部根據Class對象初始化了所有枚舉實例
        EnumSet<Color> enumSet1= EnumSet.allOf(Color.class);
        System.out.println("allOf直接填充:"+enumSet1.toString());

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

        //初始集合包括枚舉值中指定範圍的元素
        EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW);
        System.out.println("指定初始化範圍:"+enumSet2.toString());

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

        //指定補集,也就是從全部枚舉類型中去除參數集合中的元素,如下去掉上述enumSet2的元素
        EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2);
        System.out.println("指定補集:"+enumSet3.toString());

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

        //初始化時直接指定元素
        EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK);
        System.out.println("指定Color.BLACK元素:"+enumSet4.toString());
        EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN);
        System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString());

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

        //複製enumSet5容器的數據作爲初始化數據
        EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5);
        System.out.println("enumSet6:"+enumSet6.toString());

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

        List<Color> list = new ArrayList<Color>();
        list.add(Color.BLACK);
        list.add(Color.BLACK);//重複元素
        list.add(Color.RED);
        list.add(Color.BLUE);
        System.out.println("list:"+list.toString());

        //使用copyOf(Collection<E> c)
        EnumSet enumSet7=EnumSet.copyOf(list);
        System.out.println("enumSet7:"+enumSet7.toString());

        /**
         輸出結果:
         添加前:[]
         添加後:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         指定初始化範圍:[BLACK, YELLOW]
         -----------------------------------
         指定補集:[GREEN, RED, BLUE]
         -----------------------------------
         指定Color.BLACK元素:[BLACK]
         指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK]
         -----------------------------------
         enumSet6:[GREEN, BLACK]
         -----------------------------------
         list:[BLACK, BLACK, RED, BLUE]
         enumSet7:[RED, BLUE, BLACK]
         */
    }

}

noneOf(Class<E> elementType)靜態方法,主要用於創建一個空的EnumSet集合,傳遞參數elementType代表的是枚舉類型的類型信息,即Class對象。EnumSet<E> allOf(Class<E> elementType)靜態方法則是創建一個填充了elementType類型所代表的所有枚舉實例,奇怪的是EnumSet提供了多個重載形式的of方法,最後一個接受的的是可變參數,其他重載方法則是固定參數個數,EnumSet之所以這樣設計是因爲可變參數的運行效率低一些,所有在參數數據不多的情況下,強烈不建議使用傳遞參數爲可變參數的of方法,即EnumSet<E> of(E first, E... rest),其他方法就不分析了,看代碼演示即可。至於EnumSet的操作方法,則與set集合是一樣的,可以看API即可這也不過多說明。什麼時候使用EnumSet比較恰當的,事實上當需要進行位域運算,就可以使用EnumSet提到位域,如下:

public class EnumSetDemo {
    //定義位域變量
    public static final int TYPE_ONE = 1 << 0 ; //1
    public static final int TYPE_TWO = 1 << 1 ; //2
    public static final int TYPE_THREE = 1 << 2 ; //4
    public static final int TYPE_FOUR = 1 << 3 ; //8
    public static void main(String[] args){
        //位域運算
        int type= TYPE_ONE | TYPE_TWO | TYPE_THREE |TYPE_FOUR;
    }
}

諸如上述情況,我們都可以將上述的類型定義成枚舉然後採用EnumSet來裝載,進行各種操作,這樣不僅不用手動編寫太多冗餘代碼,而且使用EnumSet集合進行操作也將使代碼更加簡潔明瞭。

enum Type{
    TYPE_ONE,TYPE_TWO,TYPE_THREE,TYPE_FOUR 
}

public class EnumSetDemo {
    public static void main(String[] args){
    EnumSet set =EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR);
    }
}

其實博主認爲EnumSet最有價值的是其內部實現原理,採用的是位向量,它體現出來的是一種高效的數據處理方式,這點很值得我們去學習它。

EnumSet實現原理剖析

關於EnumSet實現原理可能會有點燒腦,內部執行幾乎都是位運算,博主將盡力使用圖片來分析,協助大家理解。

理解位向量

在分析EnumSet前有必要先了解以下位向量,顧名思義位向量就是用一個bit位(0或1)標記一個元素的狀態,用一組bit位表示一個集合的狀態,而每個位對應一個元素,每個bit位的狀態只可能有兩種,即0或1。位向量能表示的元素個數與向量的bit位長度有關,如一個int類型能表示32個元素,而一個long類型則可以表示64個元素,對於EnumSet而言採用的就long類型或者long類型數組。比如現在有一個文件中的數據,該文件存儲了N=1000000個無序的整數,需要把這些整數讀取到內存並排序再重新寫回文件中,該如何解決?最簡單的方式是用int類型來存儲每個數,並把其存入到數組(int a[m])中,再進行排序,但是這種方式將會導致存儲空間異常大,對數據操作起來效率也能成問題,那有沒更高效的方式呢?的確是有的,那就是運用位向量,我們知道一個int型的數有4個字節,也就是32位,那麼我們可以用N/32個int型數組來表示這N個數:

a[0]表示第1~32個數(0~31)
a[1]表示第33~64個數(32~63)
a[2]表示第65~96個數(64~95)
...... 以此類推

這樣,每當輸入一個數字m,我們應該先找到該數字在數組的第?個元素,也就是a[?],然後再確定在這個元素的第幾個bit位,找到後設置爲1,代表存在該數字。舉個例子來說,比如輸入40,那麼40/32爲1餘8,則應該將a[1]元素值的第9個bit位置爲1(1的二進制左移8位後就是第9個位置),表示該數字存在,40數字的表示原理圖過程如下:

大概明白了位向量表示方式後,上述過程的計算方式,通過以下方式可以計算該數存儲在數組的第?個元素和元素中第?個bit位置,爲了演示方便,我們這裏假設整第?個元素中的?爲P,餘值設置S


//m 除以 2^n 則商(P)表示爲 m >> n 
//等同於 m / 2^5 取整數 即:40 / 32 = 1 ,那麼P=1就是數組中第2個元素,即a[1]

//位操作過程如下,40的二進制
00000000 00000000 00000000 00101000

//右移5位即 n=5 , m >> 5 ,即結果轉爲10進制就是P=1
00000000 00000000 00000000 00000001

在這裏我們使用的int類型,即32位,所有2^5=32,因此n=5,由此計算出 P的值代表的是數組的第 P 個元素,接着利用下述方式計算出餘數(S),以此設置該元素值的第(S+1)個bit位爲1

//m 除以2^n 的餘數(S)表示爲 m & (2^n-1) 
//等同於: m % 2^5 取餘數 即:40 % 32 = 8

//m=40的二進制
00000000 00000000 00000000 00101000

//2^n-1(31)的二進制
00000000 00000000 00000000 00011111

// m & (2^n-1) 即40與31進行與操作得出餘數 即 S=8
00000000 00000000 00000000 00001000 

//下面是將a[1]元素值的第(8+1)個bit設置爲1,爲什麼是(8+1)不是8?因爲1左移8位就在第9個bit位了,過程如下:

//1的二進制如下:
00000000 00000000 00000000 00000001

//1 << 8 利用餘數8對1進行左移動
00000000 00000000 00000001 0000000 

//然後再與a[1]執行或操作後就可以將對應的bit位設置爲1
//a[P] |= 1 << S 見下述java實現的代碼

通過上述二進制位運算過程(關於位運算可以看博主的另一篇博文~java位運算)就可以計算出整數部分P和餘數部分S,併成功設置bit位爲1,現在利用java來實現這個運算過程如下:

//定義變量
private int[] a; //數組存儲元素的數組
private int BIT_LENGTH = 32;//默認使用int類型
private int P; //整數部分
private int S; //餘數
private int MASK =  0x1F;// 2^5 - 1
private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位長度32

計算代碼

/**
 * 置位操作,添加操作
 * @param i
 */
public void set(int i){
     P = i >> SHIFT; //結果等同  P = i / BIT_LENGTH; 取整數 ①
     S = i & MASK;   //結果等同  S = i % BIT_LENGTH; 取餘數 ②

     a[P] |= 1 << S;  //賦值設置該元素bit位爲1               ③
     //將int型變量j的第k個比特位設置爲1, 即j=j|(1<<k),上述3句合併爲一句
     //a[i >> SHIFT ] |= (1 << (i & MASK));               ④
 }

計算出P和S後,就可以進行賦值了,其中 a[P]代表數組中第P個元素,a[P] |= 1 << S 整句意思是把a[P]元素的第S+1位設置爲1,注意從低位到高位設置,即從右到左,①②③合併爲④,代碼將更佳簡潔。當然有添加操作,那麼就會有刪除操作,刪除操作過程與添加類似,只不過刪除是把相對應的bit位設置0,代表不存在該數值。

/**
* 置0操作,相當於清除元素
* @param i
*/
public void clear(int i){
   P =  i >> SHIFT; //計算位於數組中第?個元素 P = i / BIT_LENGTH;
   S =  i & MASK;   //計算餘數  S = i % BIT_LENGTH;
   //把a[P]元素的第S+1個(從低位到高位)bit位設置爲0
   a[P] &= ~(1 << S);

   //更優寫法
   //將int型變量j的第k個比特位設置爲0,即j= j&~(1<<k)
   //a[i>>SHIFT] &= ~(1<<(i &MASK));
}

與添加唯一不同的是,計算出餘數S,利用1左移S位,再取反(~)操作,最後進行與(&)操作,即將a[P]元素的第S+1個(從低位到高位)bit位設置爲0,表示刪除該數字,這個計算過程大家可以自行推算一下。這就是位向量表示法的添加和清除方法,然後我們可以利用下述的get方法判斷某個bit是否存在某個數字:

/**
 * 讀取操作,返回1代表該bit位有值,返回0代表該bit位沒值
 * @param i
 * @return
 */
public int get(int i){
    //a[i>>SHIFT] & (1<<(i&MASK));
    P = i >> SHIFT;
    S = i &  MASK;
    return Integer.bitCount(a[P] & (1 << S));
}

其中Integer.bitCount()是返回指定 int 值的二進制補碼(計算機數字的二進制表示法都是使用補碼錶示的)表示形式的 1 位的數量。位向量運算整體代碼實現如下:

package com.zejian;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Created by zejian on 2017/5/13.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 * 位向量存儲數據
 */
public class BitVetory {
    private int count;
    private int[] a; //數組
    private int BIT_LENGTH = 32;//默認使用int類型
    private int P; //整數部分
    private int S; //餘數
    private int MASK =  0x1F;// 2^5 - 1
    private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位長度32

    /**
     * 初始化位向量
     * @param count
     */
    public BitVetory(int count) {
        this.count = count;
        a = new int[(count-1)/BIT_LENGTH + 1];
        init();
    }

    /**
     * 將數組中元素bit位設置爲0
     */
    public void init(){
        for (int i = 0; i < count; i++) {
            clear(i);
        }
    }

    /**
     * 獲取排序後的數組
     * @return
     */
    public List<Integer> getSortedArray(){
        List<Integer> sortedArray = new ArrayList<Integer>();

        for (int i = 0; i < count; i++) {
            if (get(i) == 1) {//判斷i是否存在
                sortedArray.add(i);
            }
        }
        return sortedArray;
    }
    /**
     * 置位操作,設置元素
     * @param i
     */
    public void set(int i){
        P = i >> SHIFT; //P = i / BIT_LENGTH; 取整數
        S = i & MASK; //S = i % BIT_LENGTH; 取餘數
        a[P] |= 1 << S;

        //將int型變量j的第k個比特位設置爲1, 即j=j|(1<<k),上述3句合併爲一句
        //a[i >> SHIFT ] |= (1 << (i & MASK));
    }

    /**
     * 置0操作,相當於清除元素
     * @param i
     */
    public void clear(int i){
        P =  i >> SHIFT; //計算位於數組中第?個元素 P = i / BIT_LENGTH;
        S =  i & MASK;   //計算餘數  S = i % BIT_LENGTH;
        a[P] &= ~(1 << S);

        //更優寫法
        //將int型變量j的第k個比特位設置爲0,即j= j&~(1<<k)
        //a[i>>SHIFT] &= ~(1<<(i &MASK));
    }

    /**
     * 讀取操作,返回1代表該bit位有值,返回0代表該bit位沒值
     * @param i
     * @return
     */
    public int get(int i){
        //a[i>>SHIFT] & (1<<(i&MASK));
        P = i >> SHIFT;
        S = i &  MASK;
        return Integer.bitCount(a[P] & (1 << S));
    }

    //測試
    public static void main(String[] args) {
        int count = 25;
        List<Integer> randoms = getRandomsList(count);
        System.out.println("排序前:");

        BitVetory bitVetory = new BitVetory(count);
        for (Integer e : randoms) {
            System.out.print(e+",");
            bitVetory.set(e);
        }

        List<Integer> sortedArray = bitVetory.getSortedArray();
        System.out.println();
        System.out.println("排序後:");
        for (Integer e : sortedArray) {
            System.out.print(e+",");
        }

        /**
         輸出結果:
         排序前:
         6,3,20,10,18,15,19,16,13,4,21,22,24,2,14,5,12,7,23,8,1,17,9,11,
         排序後:
         1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
         */
    }

    private static List<Integer> getRandomsList(int count) {
        Random random = new Random();

        List<Integer> randomsList = new ArrayList<Integer>();
        while(randomsList.size() < (count - 1)){
            int element = random.nextInt(count - 1) + 1;//element ∈  [1,count)
            if (!randomsList.contains(element)) {
                randomsList.add(element);
            }
        }
        return randomsList;
    }
}

EnumSet原理

有前面位向量的分析,對於瞭解EnumSet的實現原理就相對簡單些了,EnumSet內部使用的位向量實現的,前面我們說過EnumSet是一個抽象類,事實上它存在兩個子類,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一個long類型的變量作爲位向量,long類型的位長度是64,因此可以存儲64個枚舉實例的標誌位,一般情況下是夠用的了,而JumboEnumSet使用一個long類型的數組,當枚舉個數超過64時,就會採用long數組的方式存儲。先看看EnumSet內部的數據結構:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    //表示枚舉類型
    final Class<E> elementType;
    //存儲該類型信息所表示的所有可能的枚舉實例
    final Enum<?>[] universe;
    //..........
}

EnumSet中有兩個變量,一個elementType用於表示枚舉的類型信息,universe是數組類型,存儲該類型信息所表示的所有可能的枚舉實例,EnumSet是抽象類,因此具體的實現是由子類完成的,下面看看noneOf(Class<E> elementType)靜態構建方法

  public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        //根據EnumMap中的一樣,獲取所有可能的枚舉實例
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            //枚舉個數小於64,創建RegularEnumSet
            return new RegularEnumSet<>(elementType, universe);
        else
            //否則創建JumboEnumSet
            return new JumboEnumSet<>(elementType, universe);
    }

從源碼可以看出如果枚舉值個數小於等於64,則靜態工廠方法中創建的就是RegularEnumSet,否則大於64的話就創建JumboEnumSet。無論是RegularEnumSet還是JumboEnumSet,其構造函數內部都間接調用了EnumSet的構造函數,因此最終的elementType和universe都傳遞給了父類EnumSet的內部變量。如下:

//RegularEnumSet構造
RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
  }

//JumboEnumSet構造
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
      elements = new long[(universe.length + 63) >>> 6];
  }

在RegularEnumSet類和JumboEnumSet類中都存在一個elements變量,用於記錄位向量的操作,

//RegularEnumSet
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 3411599620347842686L;
    //通過long類型的elements記錄位向量的操作
    private long elements = 0L;
    //.......
}

//對於JumboEnumSet則是:
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 334349849919042784L;
    //通過long數組類型的elements記錄位向量
    private long elements[];
     //表示集合大小
    private int size = 0;

    //.............
    }

在RegularEnumSet中elements是一個long類型的變量,共有64個bit位,因此可以記錄64個枚舉常量,當枚舉常量的數量超過64個時,將使用JumboEnumSet,elements在該類中是一個long型的數組,每個數組元素都可以存儲64個枚舉常量,這個過程其實與前面位向量的分析是同樣的道理,只不過前面使用的是32位的int類型,這裏使用的是64位的long類型罷了。接着我們看看EnumSet是如何添加數據的,RegularEnumSet中的add實現如下

public boolean add(E e) {
    //檢測是否爲枚舉類型
    typeCheck(e);
    //記錄舊elements
    long oldElements = elements;
    //執行位向量操作,是不是很熟悉?
    //數組版:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements |= (1L << ((Enum)e).ordinal());
    return elements != oldElements;
}

關於elements |= (1L << ((Enum)e).ordinal());這句跟我們前面分析位向量操作是相同的原理,只不過前面分析的是數組類型實現,這裏用的long類型單一變量實現,((Enum)e).ordinal()通過該語句獲取要添加的枚舉實例的序號,然後通過1左移再與 long類型的elements進行或操作,就可以把對應位置上的bit設置爲1了,也就代表該枚舉實例存在。圖示演示過程如下,注意universe數組在EnumSet創建時就初始化並填充了所有可能的枚舉實例,而elements值的第n個bit位1時代表枚舉存在,而獲取的則是從universe數組中的第n個元素值。

這就是枚舉實例的添加過程和獲取原理。而對於JumboEnumSet的add實現則是如下:

public boolean add(E e) {
    typeCheck(e);
    //計算ordinal值
    int eOrdinal = e.ordinal();
    int eWordNum = eOrdinal >>> 6;

    long oldElements = elements[eWordNum];
    //與前面分析的位向量相同:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements[eWordNum] |= (1L << eOrdinal);
    boolean result = (elements[eWordNum] != oldElements);
    if (result)
        size++;
    return result;
}

關於JumboEnumSet的add實現與RegularEnumSet區別是一個是long數組類型,一個long變量,運算原理相同,數組的位向量運算與前面分析的是相同的,這裏不再分析。接着看看如何刪除元素

//RegularEnumSet類實現
public boolean remove(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    long oldElements = elements;
    //將int型變量j的第k個比特位設置爲0,即j= j&~(1<<k)
    //數組類型:a[i>>SHIFT] &= ~(1<<(i &MASK));

    elements &= ~(1L << ((Enum)e).ordinal());//long遍歷類型操作
    return elements != oldElements;
}


//JumboEnumSet類的remove實現
public boolean remove(Object e) {
        if (e == null)
            return false;
        Class<?> eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        int eOrdinal = ((Enum<?>)e).ordinal();
        int eWordNum = eOrdinal >>> 6;

        long oldElements = elements[eWordNum];
        //與a[i>>SHIFT] &= ~(1<<(i &MASK));相同
        elements[eWordNum] &= ~(1L << eOrdinal);
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size--;
        return result;
    }

刪除remove的實現,跟位向量的清空操作是同樣的實現原理,如下:

至於JumboEnumSet的實現原理也是類似的,這裏不再重複。下面爲了簡潔起見,我們以RegularEnumSet類的實現作爲源碼分析,畢竟JumboEnumSet的內部實現原理可以說跟前面分析過的位向量幾乎一樣。o~,看看如何判斷是否包含某個元素

public boolean contains(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;
    //先左移再按&操作
    return (elements & (1L << ((Enum)e).ordinal())) != 0;
}

public boolean containsAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet))
        return super.containsAll(c);

    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
    if (es.elementType != elementType)
        return es.isEmpty();
    //~elements取反相當於elements補集,再與es.elements進行&操作,如果爲0,
    //就說明elements補集與es.elements沒有交集,也就是es.elements是elements的子集
    return (es.elements & ~elements) == 0;
}

對於contains(Object e) 方法,先左移再按位與操作,不爲0,則表示包含該元素,跟位向量的get操作實現原理類似,這個比較簡單。對於containsAll(Collection<?> c)則可能比較難懂,這裏分析一下,elements變量(long類型)標記EnumSet集合中已存在元素的bit位,如果bit位爲1則說明存在枚舉實例,爲0則不存在,現在執行~elements 操作後 則說明~elements是elements的補集,那麼只要傳遞進來的es.elements與補集~elements 執行&操作爲0,那麼就可以證明es.elements與補集~elements 沒有交集的可能,也就是說es.elements只能是elements的子集,這樣也就可以判斷出當前EnumSet集合中包含傳遞進來的集合c了,藉着下圖協助理解:

圖中,elements代表A,es.elements代表S,~elements就是求A的補集,(es.elements & ~elements) == 0就是在驗證A’∩B是不是空集,即S是否爲A的子集。接着看retainAll方法,求兩個集合交集

public boolean retainAll(Collection<?> c) {
        if (!(c instanceof RegularEnumSet))
            return super.retainAll(c);

        RegularEnumSet<?> es = (RegularEnumSet<?>)c;
        if (es.elementType != elementType) {
            boolean changed = (elements != 0);
            elements = 0;
            return changed;
        }

        long oldElements = elements;
        //執行與操作,求交集,比較簡單
        elements &= es.elements;
        return elements != oldElements;
    }

最後來看看迭代器是如何取值的

 public Iterator<E> iterator() {
        return new EnumSetIterator<>();
    }

    private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
        //記錄elements
        long unseen;

        //記錄最後一個返回值
        long lastReturned = 0;

        EnumSetIterator() {
            unseen = elements;
        }

        public boolean hasNext() {
            return unseen != 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (unseen == 0)
                throw new NoSuchElementException();
            //取值過程,先與本身負執行&操作得出的就是二進制低位開始的第一個1的數值大小
            lastReturned = unseen & -unseen;
            //取值後減去已取得lastReturned
            unseen -= lastReturned;
            //返回在指定 long 值的二進制補碼錶示形式中最低位(最右邊)的 1 位之後的零位的數量
            return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
        }

        public void remove() {
            if (lastReturned == 0)
                throw new IllegalStateException();
            elements &= ~lastReturned;
            lastReturned = 0;
        }
    }

比較晦澀的應該是

//取值過程,先與本身負執行&操作得出的就是二進制低位開始的第一個1的數值大小
lastReturned = unseen & -unseen; 
//取值後減去已取得lastReturned
unseen -= lastReturned;
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];

我們通過原理圖來協助理解,現在假設集合中已保存所有可能的枚舉實例變量,我們需要把它們遍歷展示出來,下面的第一個枚舉元素的獲取過程,顯然通過unseen & -unseen;操作,我們可以獲取到二進制低位開始的第一個1的數值,該計算的結果是要麼全部都是0,要麼就只有一個1,然後賦值給lastReturned,通過Long.numberOfTrailingZeros(lastReturned)獲取到該bit爲1在64位的long類型中的位置,即從低位算起的第幾個bit,如圖,該bit的位置恰好是低位的第1個bit位置,也就指明瞭universe數組的第一個元素就是要獲取的枚舉變量。執行unseen -= lastReturned;後繼續進行第2個元素的遍歷,依次類推遍歷出所有值,這就是EnumSet的取值過程,真正存儲枚舉變量的是universe數組,而通過long類型變量的bit位的0或1表示存儲該枚舉變量在universe數組的那個位置,這樣做的好處是任何操作都是執行long類型變量的bit位操作,這樣執行效率將特別高,畢竟是二進制直接執行,只有最終獲取值時纔會操作到數組universe。

ok~,到這關於EnumSet的實現原理主要部分我們就分析完了,其內部使用位向量,存儲結構很簡潔,節省空間,大部分操作都是按位運算,直接操作二進制數據,因此效率極高。當然通過前面的分析,我們也掌握位向量的運算原理。好~,關於java枚舉,我們暫時聊到這。


參考資料 《Thinking in Java》 And 《Effective Java》



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