1. 程式人生 > >重新認識java(十) ---- Enum(列舉類)

重新認識java(十) ---- Enum(列舉類)

有的人說,不推薦使用列舉。有的人說,列舉很好用。究竟怎麼使用,如何使用,仁者見仁智者見智。總之,先學會再說~

為什麼要引入列舉類

一個小案例

你寫了一個小程式,不過好久不用了,突然有一天,你想使用一下它。程式要想正確執行,需要將今天星期幾存到資料庫裡。這個時候,你開始犯難了。

當初的你還很年輕,不懂程式界的險惡,設計這個程式的時候,傻不拉幾把這個欄位設計為int型別的,用0代表週日,1代表週一。。。6代表週六,新增的時候就setWeekday(0)。但是這麼長時間沒用了,你忘記自己是從週一開始計算還是週日開始計算了,換句話說,你想不起來0代表的是週一還是週日了!

於是你各種翻程式碼,看資料庫的欄位,資料庫儲存的資訊,終於搞懂了,你很開心,用了一次之後,覺得這個程式沒意思,又不用了。

很久之後,你心血來潮,又想用一次它,很不幸,你又忘記到底0代表週一還是週日了,一番查詢之後。你決定重構程式碼,因為你受不了了!!

靜態變數來幫忙

經過一番思考,你決定使用七個靜態變數來代表星期幾,以後只要引用和靜態變數就可以了,而不用自己輸入012….你這麼寫:

public class Weekday {
    public final static int SUN = 0;
    public final static int MON = 1;
    public final static int TUE = 2;
    public final static int WED = 3
; public final static int THU = 4; public final static int FRI = 5; public final static int SAT = 6; }

機智如你,這個時候,只要Weekday.SUN就可以了,不用操心到底應該填寫0還是填寫1。

但是這個時候的你,也不是當初初出茅廬的小夥子了,很明顯,這樣寫已經不能滿足你了。你還想讓這個類做更多的事,比如,你想知道下一天是星期幾,還想把今天是星期幾打印出來。一番深思熟慮後,你改成了這樣:

public class Weekday {

    private Weekday
(){} public final static Weekday SUN = new Weekday(); public final static Weekday MON = new Weekday(); public final static Weekday TUE = new Weekday(); public final static Weekday WED = new Weekday(); public final static Weekday THU = new Weekday(); public final static Weekday FRI = new Weekday(); public final static Weekday SAT = new Weekday(); public static Weekday getNextDay(Weekday nowDay){ if(nowDay == SUN) { return MON; }else if(nowDay == MON) { return TUE; }else if(nowDay == TUE) { return WED; }else if(nowDay == WED) { return THU; }else if(nowDay == THU) { return FRI; }else if(nowDay == FRI) { return SAT; }else { return SUN; } } public static void printNowDay(Weekday nowDay){ if(nowDay == SUN) System.out.println("sunday"); else if(nowDay == MON) System.out.println("monday"); else if(nowDay == TUE) System.out.println("tuesday"); else if(nowDay == WED) System.out.println("wednesday"); else if(nowDay == THU) System.out.println("thursday"); else if(nowDay == FRI) System.out.println("friday"); else System.out.println("saturday"); } } class Test1{ public static void main(String[] args) { Weekday nowday = Weekday.SUN; Weekday.printNowDay(nowday); Weekday nextDay = Weekday.getNextDay(nowday); System.out.print("nextday ====> "); Weekday.printNowDay(nextDay); } } //測試結果: //sunday //nextday ====> monday

喲,不錯。考慮的很詳細。並且私有構造方法後,外界就不能建立該類的物件了,這樣就避免了星期八星期九的出現,所有Weekday的物件都在該類內部建立。

不對,好像缺了點什麼,我要的是int!我的int呢?!。所以,你還需要一個這樣的方法:

    public static int toInt(Weekday nowDay){
        if(nowDay == SUN)
            return 0;
        else if(nowDay == MON)
            return 1;
        else if(nowDay == TUE)
            return 2;
        else if(nowDay == WED)
            return 3;
        else if(nowDay == THU)
            return 4;
        else if(nowDay == FRI)
            return 5;
        else
           return 6;
    }

當你需要一個整形資料的時候,只需要Weekday.toInt(Weekday.SUN);,看起來你好像完成了你的任務。

但是,你有沒有發現,這樣寫,好麻煩啊。如果想要擴充套件一下功能,大量的ifelse會讓人眼花繚亂。

有沒有更好的方式呢?你大概已經知道了,沒錯,我們需要列舉類!

我們先來看看列舉類是什麼。

一個簡單的列舉類

話不多說,先來程式碼:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

程式碼這麼少?

沒錯,這就是列舉類,我們來看看怎麼使用它:

class Test2{
    public static void main(String[] args) {
        Weekday sun = Weekday.SUN;
        System.out.println(sun); // 輸出 SUN
    }
}

看起來和上面的靜態變數使用方式差不多,而且預設的toString方法返回的就是對應的名字。

我們上面的那段程式碼重寫toString也是不可以打印出當前是星期幾的,因為toString方法沒有引數。所以我們自己寫了一個printNowDay方法。

當然,這麼簡單的列舉類是不可能實現我們的要求的,所以,我們還要接著寫:

public enum Weekday {
    SUN(0),MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6);

    private int value;

    private Weekday(int value){
        this.value = value;
    }

    public static Weekday getNextDay(Weekday nowDay){
        int nextDayValue = nowDay.value;

        if (++nextDayValue == 7){
            nextDayValue =0;
        }

        return getWeekdayByValue(nextDayValue);
    }

    public static Weekday getWeekdayByValue(int value) {
        for (Weekday c : Weekday.values()) {
            if (c.value == value) {
                return c;
            }
        }
        return null;
    }
}

class Test2{
    public static void main(String[] args) {
        System.out.println("nowday ====> " + Weekday.SAT);
        System.out.println("nowday int ====> " + Weekday.SAT.ordinal());
        System.out.println("nextday ====> " + Weekday.getNextDay(Weekday.SAT)); // 輸出 SUN

        //輸出:
        //nowday ====> SAT
        //nowday int ====> 6
        //nextday ====> SUN
    }
}

這樣就完成了我們的目標,和之前的程式碼比起來,有沒有覺得突然高大上了許多?沒有那麼多煩人的ifelse,世界都清淨了。

好了,現在你大概知道為什麼要引入列舉類了吧?就是因為在沒有列舉類的時候,我們要定義一個有限的序列,比如星期幾,男人女人,春夏秋冬,一般會通過上面那種靜態變數的形式,但是使用那樣的形式如果需要一些其他的功能,需要些很多奇奇怪怪的程式碼。所以,列舉類的出現,就是為了簡化這種操作。

可以將列舉類理解為是java的一種語法糖。

列舉類的用法

最簡單的使用

最簡單的列舉類就像我們上面第一個定義的列舉類一樣:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

如何使用它呢?

先來看看它有哪些方法:

Weekday方法

這是Weekday可以呼叫的方法和引數。發現它有兩個方法:value()和valueOf()。還有我們剛剛定義的七個變數。

列舉變數的方法

這些事列舉變數的方法。我們接下來會演示幾個比較重要的:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

class Test3{
    public static void main(String[] args) {
        System.out.println(Weekday.valueOf("mon".toUpperCase()));
        //MON

        for (Weekday w : Weekday.values()){
            System.out.println(w + ".ordinal()  ====>" +w.ordinal());
        }
        //SUN.ordinal()  ====>0
        //MON.ordinal()  ====>1
        //TUS.ordinal()  ====>2
        //WED.ordinal()  ====>3
        //THU.ordinal()  ====>4
        //FRI.ordinal()  ====>5
        //SAT.ordinal()  ====>6

        System.out.println("Weekday.MON.compareTo(Weekday.FRI) ===> " + Weekday.MON.compareTo(Weekday.FRI));
        System.out.println("Weekday.MON.compareTo(Weekday.MON) ===> " + Weekday.MON.compareTo(Weekday.MON));
        System.out.println("Weekday.MON.compareTo(Weekday.SUM) ===> " + Weekday.MON.compareTo(Weekday.SUN));
        //Weekday.MON.compareTo(Weekday.FRI) ===> -4
        //Weekday.MON.compareTo(Weekday.MON) ===> 0
        //Weekday.MON.compareTo(Weekday.SUM) ===> 1

        System.out.println("Weekday.MON.name() ====> " + Weekday.MON.name());
        //Weekday.MON.name() ====> MON

    }
}

這段程式碼,我們演示了幾個常用的方法和功能:

  1. Weekday.valueOf() 方法:

    它的作用是傳來一個字串,然後將它轉變為對應的列舉變數。前提是你傳的字串和定義列舉變數的字串一抹一樣,區分大小寫。如果你傳了一個不存在的字串,那麼會丟擲異常。

  2. Weekday.values()方法。

    這個方法會返回包括所有列舉變數的陣列。在該例中,返回的就是包含了七個星期的Weekday[]。可以方便的用來做迴圈。

  3. 列舉變數的toString()方法。

    該方法直接返回列舉定義列舉變數的字串,比如MON就返回【”MON”】。

  4. 列舉變數的.ordinal()方法。

    預設請款下,列舉類會給所有的列舉變數一個預設的次序,該次序從0開始,類似於陣列的下標。而.ordinal()方法就是獲取這個次序(或者說下標)

  5. 列舉變數的compareTo()方法。

    該方法用來比較兩個列舉變數的”大小”,實際上比較的是兩個列舉變數的次序,返回兩個次序相減後的結果,如果為負數,就證明變數1”小於”變數2 (變數1.compareTo(變數2),返回【變數1.ordinal() - 變數2.ordinal()】)

    compareTo原始碼

    這是compareTo的原始碼,會先判斷是不是同一個列舉類的變數,然後再返回差值。

  6. 列舉類的name()方法。

    它和toString()方法的返回值一樣,事實上,這兩個方法本來就是一樣的:
    name方法

    toString方法

    這兩個方法的預設實現是一樣的,唯一的區別是,你可以重寫toString方法。name變數就是列舉變數的字串形式。

還有一些其他的方法我就暫時不介紹了,感興趣的話可以自己去看看文件或者原始碼,都挺簡單的。

要點:

  • 使用的是enum關鍵字而不是class。

  • 多個列舉變數直接用逗號隔開。

  • 列舉變數最好大寫,多個單詞之間使用”_”隔開(比如:INT_SUM)。

  • 定義完所有的變數後,以分號結束,如果只有列舉變數,而沒有自定義變數,分號可以省略(例如上面的程式碼就忽略了分號)。

  • 在其他類中使用enum變數的時候,只需要【類名.變數名】就可以了,和使用靜態變數一樣。

但是這種簡單的使用顯然不能體現出列舉的強大,我們來學習一下複雜的使用:

列舉的高階使用方法

就像我們前面的案例一樣,你需要讓每一個星期幾對應到一個整數,比如星期天對應0。上面講到了,列舉類在定義的時候會自動為每個變數新增一個順序,從0開始。

假如你希望0代表星期天,1代表週一。。。並且你在定義列舉類的時候,順序也是這個順序,那你可以不用定義新的變數,就像這樣:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

這個時候,星期天對應的ordinal值就是0,週一對應的就是1,滿足你的要求。但是,如果你這麼寫,那就有問題了:

public enum Weekday {
    MON,TUS,WED,THU,FRI,SAT,SUN
}

我吧SUN放到了最後,但是我還是希0代表SUN,1代表MON怎麼辦呢?預設的ordinal是指望不上了,因為它只會傻傻的給第一個變數0,給第二個1。。。

所以,我們需要自己定義變數!

看程式碼:

public enum Weekday {
    MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6),SUN(0);

    private int value;

    private Weekday(int value){
        this.value = value;
    }
}

我們對上面的程式碼做了一些改變:

首先,我們在每個列舉變數的後面加上了一個括號,裡面是我們希望它代表的數字。

然後,我們定義了一個int變數,然後通過建構函式初始化這個變數。

你應該也清楚了,括號裡的數字,其實就是我們定義的那個int變數。這句叫做自定義變數。

請注意:這裡有三點需要注意:

  1. 一定要把列舉變數的定義放在第一行,並且以分號結尾。

  2. 建構函式必須私有化。事實上,private是多餘的,你完全沒有必要寫,因為它預設並強制是private,如果你要寫,也只能寫private,寫public是不能通過編譯的。

  3. 自定義變數與預設的ordinal屬性並不衝突,ordinal還是按照它的規則給每個列舉變數按順序賦值。

好了,你很聰明,你已經掌握了上面的知識,你想,既然能自定義一個變數,能不能自定義兩個呢?

當然可以:

public enum Weekday {
    MON(1,"mon"),TUS(2,"tus"),WED(3,"wed"),THU(4,"thu"),FRI(5,"fri"),SAT(6,"sat"),SUN(0,"sun");

    private int value;
    private String label;

    private Weekday(int value,String label){
        this.value = value;
        this.label = label;
    }
}

你可以定義任何你想要的變數。學完了這些,大概列舉類你也應該掌握了,但是,還有沒有其他用法呢?

列舉類中的抽象類

如果我在列舉類中定義一個抽象方法會怎麼樣?

你要知道,列舉類不能繼承其他類,也不能被其他類繼承。至於為什麼,我們後面會說到。

你應該知道,有抽象方法的類必然是抽象類,抽象類就需要子類繼承它然後實現它的抽象方法,但是呢,列舉類不能被繼承。。你是不是有點亂?

我們先來看程式碼:

public enum TrafficLamp {
    RED(30) {
        @Override
        public TrafficLamp getNextLamp() {
            return GREEN;
        }
    }, GREEN(45) {
        @Override
        public TrafficLamp getNextLamp() {
            return YELLOW;
        }
    }, YELLOW(5) {
        @Override
        public TrafficLamp getNextLamp() {
            return RED;
        }
    };

    private int time;

    private TrafficLamp(int time) {
        this.time = time;
    }

    //一個抽象方法
    public abstract TrafficLamp getNextLamp();

}

你好像懂了點什麼。但是你好像又不太懂。為什麼一個變數的後邊可以帶一個程式碼塊並且實現抽象方法呢?

彆著急,帶著這個疑問,我們來看一下列舉類的實現原理。

列舉類的實現原理

從最簡單的看起:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

還是這段熟悉的程式碼,我們編譯一下它,再反編譯一下看看它到底是什麼樣子的:

反編譯程式碼

你是不是覺得很熟悉?反編譯出來的程式碼和我們一開始用靜態變數自己寫的那個類出奇的相似!

而且,你看到了熟悉的values()方法和valueOf()方法。

仔細看,這個類繼承了java.lang.Enum類!所以說,列舉類不能再繼承其他類了,因為預設已經繼承了Enum類。

並且,這個類是final的!所以它不能被繼承!

回到我們剛才的那個疑問:

RED(30) {
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
}

為什麼會有這麼神奇的程式碼?現在你差不多懂了。因為RED本身就是一個TrafficLamp物件的引用。實際上,在初始化這個列舉類的時候,你可以理解為執行的是TrafficLamp RED = new TrafficLamp(30) ,但是因為TrafficLamp裡面有抽象方法,還記得匿名內部類麼?

我們可以這樣來建立一個TrafficLamp引用:

TrafficLamp RED = new TrafficLamp(30){
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
};

而在列舉類中,我們只需要像上面那樣寫【RED(30){}】就可以了,因為java會自動的去幫我們完成這一系列操作。

如果你還是不太理解,那麼你可以自己去反編譯一下TrafficLamp這個類,看看jvm是怎麼處理它的就明白了。

列舉類的其他用法

說一說列舉類的其他用法。

switch語句中使用

enum Signal {
    GREEN, YELLOW, RED
}

public class TrafficLight {
    Signal color = Signal.RED;

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

實現介面

雖然列舉類不能繼承其他類,但是還是可以實現介面的

public interface Behaviour {
    void print();

    String getInfo();
}

public enum Color implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
    // 成員變數
    private String name;
    private int index;

    // 構造方法
    private Color(String name, int index) {
        this.name = name;
        this.index = index;
    }

    // 介面方法
    @Override
    public String getInfo() {
        return this.name;
    }

    // 介面方法
    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }
}

使用介面組織列舉

public interface Food {
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
    }

    enum Dessert implements Food {
        FRUIT, CAKE, GELATO
    }
}

使用列舉建立單例模式

使用列舉建立的單例模式:

public enum EasySingleton{
    INSTANCE;
}

程式碼就這麼簡單,你可以使用EasySingleton.INSTANCE呼叫它,比起你在單例中呼叫getInstance()方法容易多了。

我們來看看正常情況下是怎樣建立單例模式的:

用雙檢索實現單例:

下面的程式碼是用雙檢索實現單例模式的例子,在這裡getInstance()方法檢查了兩次來判斷INSTANCE是否為null,這就是為什麼叫雙檢索的原因,記住雙檢索在java5之前是有問題的,但是java5在記憶體模型中有了volatile變數之後就沒問題了。

public class DoubleCheckedLockingSingleton{
     private volatile DoubleCheckedLockingSingleton INSTANCE;

     private DoubleCheckedLockingSingleton(){}

     public DoubleCheckedLockingSingleton getInstance(){
         if(INSTANCE == null){
            synchronized(DoubleCheckedLockingSingleton.class){
                //double checking Singleton instance
                if(INSTANCE == null){
                    INSTANCE = new DoubleCheckedLockingSingleton();
                }
            }
         }
         return INSTANCE;
     }
}

你可以訪問DoubleCheckedLockingSingleTon.getInstance()來獲得例項物件。

用靜態工廠方法實現單例:

public class Singleton{
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getSingleton(){
        return INSTANCE;
    }
}

你可以呼叫Singleton.getInstance()方法來獲得例項物件。

上面的兩種方式就是懶漢式和惡漢式單利的建立,但是無論哪一種,都不如列舉來的方便。而且傳統的單例模式的另外一個問題是一旦你實現了serializable介面,他們就不再是單例的了。但是列舉類的父類【Enum類】實現了Serializable介面,也就是說,所有的列舉類都是可以實現序列化的,這也是一個優點。

總結

最後總結一下:

  • 可以建立一個enum類,把它看做一個普通的類。除了它不能繼承其他類了。(java是單繼承,它已經繼承了Enum),可以新增其他方法,覆蓋它本身的方法

  • switch()引數可以使用enum

  • values()方法是編譯器插入到enum定義中的static方法,所以,當你將enum例項向上轉型為父類Enum是,values()就不可訪問了。解決辦法:在Class中有一個getEnumConstants()方法,所以即便Enum介面中沒有values()方法,我們仍然可以通過Class物件取得所有的enum例項

  • 無法從enum繼承子類,如果需要擴充套件enum中的元素,在一個介面的內部,建立實現該介面的列舉,以此將元素進行分組。達到將列舉元素進行分組。

  • enum允許程式設計師為eunm例項編寫方法。所以可以為每個enum例項賦予各自不同的行為。

本文到這裡就差不多結束了。可能舉得例子不是很恰當,程式碼寫的不是很優雅,不過我只是用來引出列舉的,大家不要雞蛋裡頭挑骨頭哈哈。

如果文章內容有什麼問題,請及時與我聯絡。

除此之外,還有兩個列舉集合:【java.util.EnumSet和java.util.EnumMap】沒有講。關於列舉集合的使用會在後面講集合框架的時候再詳細講解。

看完了,如果對你有幫助,隨手點個讚唄~~~