天天看點

[轉載] Java中枚舉類型的使用 - enum

本文對Java中的枚舉類型(enum)作了比較詳細的介紹, 并作了代碼示例, 包括JVM在編譯時添加的特性、向枚舉類中添加方法、接口内部建立枚舉、枚舉類中使用枚舉等方面.

目錄

  • 1 枚舉類的編譯特性
    • 1.1 枚舉類示範
    • 1.2 反編譯檢視編譯器的操作
  • 2 向枚舉類中添加方法
  • 3 枚舉類中使用抽象方法
  • 4 接口内部建立枚舉
  • 5 枚舉類中使用枚舉
  • 6 擴充: 驗證values()不是通過父類繼承的
  • 版權聲明

本文轉載自部落格 - Java枚舉類型, 部落客對原文内容及結構作了一定的修改.

修改時參考到的文章: 深入了解Java枚舉類型(enum).

從JDK 5開始, Java中多了一個關鍵字 ——

enum

: 可以将一組具有名稱的值(包括String、Integer等)的有限集合建立為一種新的類型, 而這些具名的值可以作為正常的程式元件使用.

這些具名的值稱為枚舉值, 這種新的類型稱為枚舉類型.

下面是一個簡單的表示星期幾的枚舉類:

enum Day {
    // 結尾處可以不用“;”, 但若有其他方法, 必須通過“;”結束枚舉執行個體的聲明
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
           

建立enum時, 編譯器會自動為enum類添加一些特性, 比如:

a) 建立

toString()

方法: 以便顯示某個枚舉執行個體的名字;

b) 建立

name()

方法: 擷取枚舉類型中某個執行個體的名稱;

c) 建立

ordinal()

方法: 表示某個特定枚舉常量的申明順序, 從0開始;

d) 建立

values()

方法: 按照枚舉常量的申明順序産生這些常量構成的數組…...

關于這些特性, 編寫測試方法如下:

public class EnumTest {
    public static void main(String[] args) {
        // 擷取枚舉類型的父類型
        System.out.println(Day.class.getSuperclass());
        // 周遊枚舉類型中的值
        for (Day day : Day.values()) {
            System.out.println(day.name() + ", ordinal: " + day.ordinal());
        }
    }
}
           

程式的輸出結果是:

class java.lang.Enum   // 所有enum的父類都是它
SUNDAY, ordinal: 0     // ordinal()方法從0開始計數
MONDAY, ordinal: 1
TUESDAY, ordinal: 2
WEDNESDAY, ordinal: 3
THURSDAY, ordinal: 4
FRIDAY, ordinal: 5
SATURDAY, ordinal: 6
           

從輸出結果可以看到: 枚舉類型Day的父類是

java.lang.Enum

——我們的代碼中并沒有指明

extends

動作, 是以這是由編譯器完成的.

(1) 利用

javac

編譯

EnumTest.java

檔案後, 會生成

Day.class

EnumTest.class

檔案, 這裡的

Day.class

就是枚舉類型 —— 使用關鍵字

enum

定義枚舉類型并編譯後, 編譯器會自動生成一個與枚舉相關的類;

(2) 反編譯檢視

Day.class

檔案:

// final修飾的類, 不允許再被繼承
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/healchow/java/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
     });
 }
}
           

(3) 編譯器生成的靜态方法:

values()

: 擷取枚舉類中的所有變量, 并作為數組傳回;

valueOf(String name)

: 與Enum類中的

valueOf()

的作用類似 —— 根據名稱擷取枚舉變量, 隻不過編譯器生成的

valueOf()

更簡潔: 隻需傳遞一個參數.

==> 這兩個方法是編譯器自動加入的, Enum類中并沒有, 是以如果我們将枚舉執行個體向上轉型為Enum, 那這兩個方法就無法被調用了.

(4) 枚舉類不能繼承其他任何類:

由于枚舉類已經繼承了

java.lang.Enum

(是一個抽象類), 而Java又不支援多繼承, 是以

enum

不能再繼承其他的類, 但是能實作接口. —— 這在反編譯後的資訊中可以看到, 編譯器為enum生成的class被final修飾, 也就是不允許被繼承.

==> 是以,

enum

隻是看起來像一種新的資料類型, 除了上面講到的這些特殊的編譯行為, 并沒有什麼特殊的地方.

除了不能繼承一個

enum

外, 基本上可以把

enum

當作一個普通的類來處理, 也就是說可以向

enum

中添加方法, 比如傳回其自身描述的方法, 還可以添加main方法.

下面是一個示範

enum

添加自定義方法和實作接口的示例:

(1) 定義一個對象描述資訊的接口:

interface ObjectDescription {
    String todo();
}
           

(2) 建立枚舉類:

public enum Signal implements ObjectDescription {
    // 結尾處可以不用“;”, 但若有其他方法, 必須通過“;”結束枚舉執行個體的聲明
    Red("紅燈", "敢過去就是6分, 還要罰款哦"), 
    Yellow("黃燈", "黃燈你也不敢過"), 
    Green("綠燈", "綠燈也得小心過啊");

    // 其他屬性、方法都必須定義在枚舉執行個體的聲明之後, 否則編譯器将報錯
    private String name;
    private String description;

   /**
    * 構造方法, 對内部變量進行初始化
    */
    Signal(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    @Override
    public String todo() {
        return "Signal類用于表示交通信号燈, [" + this + "] 表示 [" + this.getName() + "]";
    }

    public static void main(String[] args) {
        // 調用實作的接口中的方法
        for (Signal signal : Signal.values()) {
            System.out.println(signal.todo());
        }
        // 調用自定義的方法
        for (Signal signal : Signal.values()) {
            System.out.println(signal.getName() + ": " + signal.getDescription());
        }
    }
}
           

(3) 運作結果如下:

Signal類用于表示交通信号燈, [Red] 表示 [紅燈]
Signal類用于表示交通信号燈, [Yellow] 表示 [黃燈]
Signal類用于表示交通信号燈, [Green] 表示 [綠燈]
紅燈: 敢過去就是6分, 還要罰款哦
黃燈: 黃燈你也不敢過
綠燈: 綠燈也得小心過啊
           

使用注意事項:

a) 如果要自定義方法, 就必須在enum執行個體序列的最後添加一個分号, 同時Java要求必須先定義enum執行個體, 否則編譯時會報錯.

b)

enum

的構造器隻能是

private

, 它隻能在

enum

内部被用來建立

enum

執行個體, 一旦

enum

定義結束, 編譯器就不允許再使用其構造器來建立任何執行個體了.

與正常抽象類一樣, 枚舉類允許我們為其定義抽象方法, 然後靈每個枚舉執行個體都實作該方法, 以便産生不同的行為方式.

注意:

abstract

關鍵字對于枚舉類來說并不是必須的.
public enum EnumTest {
    // 枚舉執行個體的聲明必須在最前
    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("First: " + EnumTest.FIRST.getInfo());    // First: FIRST TIME
        System.out.println("Second: " + EnumTest.SECOND.getInfo());  // Second: SECOND TIME
    }
}
           

上述方式為每個枚舉執行個體定義了不同的行為.

我們可能注意到, 枚舉類的執行個體似乎表現出了多态的特性, 可惜的是枚舉類型的執行個體終究不能作為類型傳遞使用, 就像下面的使用方式, 是不能通過編譯器的檢查的:

// 無法通過編譯, 畢竟EnumTest.FIRST是個執行個體對象
public void text(EnumTest.FIRST instance){ }
           

無法使用繼承限制了枚舉的使用, 比如需要用

enum

表示食物, 但同時需要區分水果、點心等類型, 這個時候就沒不夠靈活了.

我們通過在一個接口内部建立實作該接口的枚舉, 進而達到對元素進行分類組織的目的:

public interface Food {
    /**
     * 開胃菜
     */
    enum Appetizer implements Food {
        // 結尾處可以不用“;”, 但若有其他方法, 必須通過“;”結束枚舉執行個體的聲明
        SALAD, SOUP, SPRING_ROLLS
    }
  
    /**
     * 主菜
     */
    enum MainCourse implements Food {
        RICE, NOODLES, VINDALOO, BEEF
    }

    /**
     * 甜品
     */
    enum Dessert implements Food {
        TIRAMISU, ICE_CREAM, BISCUIT, FRUIT
    }
  
    /**
     * 咖啡
     */
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE
    }
}
           

enum

而言, 實作接口是使其子類化的唯一方法.

通過上面的形式, 成功地完成了對不同食物的分組, 并且它們都是Food.

下面是一個枚舉的随機選擇器, 是一個工具類:

public class Enums {
    private static Random rand = new Random(47);

    public static <T extends Enum<T>> T random(Class<T> enumClazz) {
        return random(enumClazz.getEnumConstants());
    }

    public static <T> T random(T[] values) {
        return values[rand.nextInt(values.length)];
    }
}
           

結合工具類及上面Food接口的内容, 下面通過枚舉的枚舉實作一個産生随機菜單的例子:

public enum Course {
    // 結尾處可以不用“;”, 但若有其他方法, 必須通過“;”結束枚舉執行個體的聲明
    APPETIZER(Food.Appetizer.class),
    MAINCOURSE(Food.MainCourse.class), 
    DESSERT(Food.Dessert.class), 
    COFFEE(Food.Coffee.class);
    
    // 其他屬性、方法都必須定義在枚舉執行個體的聲明之後, 否則編譯器将報錯
    private Food[] values;

    Course(Class<? extends Food> kind) {
        // 傳回枚舉中所有的元素, 及所有執行個體構成的數組, 如果kind不是枚舉傳回null
        values = kind.getEnumConstants();
    }

    public Food randomSelection() {
        return Enums.random(values);
    }

    public static void main(String[] args) {
        // 産生5份随機菜單
        for (int i = 0; i < 5; i++) {
            for (Course c : Course.values()) {
                Food food = c.randomSelection();
                System.out.println(food + "  ");
            }
            System.out.println("---------------");
        }
    }
}
           

下面的代碼用來驗證

values()

方法是

enum

自身的, 而不是繼承自父類

java.lang.Enum

的:

public enum Signal implements ObjectDescription {
    // 結尾處可以不用“;”, 但若有其他方法, 必須通過“;”結束枚舉執行個體的聲明
    Red("紅燈", "敢過去就是6分, 還要罰款哦"), 
    Yellow("黃燈", "黃燈你也不敢過"), 
    Green("綠燈", "綠燈也得小心過啊");

    // 其他屬性、方法都必須定義在枚舉執行個體的聲明之後, 否則編譯器将報錯
    private String name;
    private String description;

    Signal(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    @Override
    public String todo() {
        return "Signal類用于表示交通信号燈, [" + this + "] 表示 [" + this.getName() + "]";
    }

    public static void main(String[] args) {
        Set<Method> methodSet = new HashSet<Method>();
        // 擷取本類的所有方法
        Method[] signalMethods = Signal.class.getMethods();
        for (Method m : signalMethods) {
            methodSet.add(m);
        }
        // 擷取父類中的方法
        Method[] superClassMethods = Signal.class.getSuperclass().getMethods();
        // 去除本類中繼承的父類方法
        for (Method m : superClassMethods) {
            methodSet.remove(m);
        }
        // 周遊輸出本類中獨有的方法
        for(Method m : methodSet) {
            System.out.println(m);
        }
    }
}
           

結果如下, 其中各個字段的含義依次為

通路權限 [是否靜态] 傳回值類型的全限定名稱 方法的全限定名稱

:

public static com.healchow.Signal com.healchow.Signal.valueOf(java.lang.String)
public static com.healchow.Signal[] com.healchow.Signal.values()
public static void com.healchow.Signal.main(java.lang.String[])
public java.lang.String com.healchow.Signal.todo()
           

本文版權歸原作者所有,如有侵權,請聯系部落客,定當立即删除。

感謝閱讀,公衆号 「瘦風的南牆」 ,手機端閱讀更佳,還有其他福利和心得輸出,歡迎掃碼關注🤝