本文對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()
: 擷取枚舉類中的所有變量, 并作為數組傳回;
②
: 與Enum類中的
valueOf(String name)
的作用類似 —— 根據名稱擷取枚舉變量, 隻不過編譯器生成的
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()
本文版權歸原作者所有,如有侵權,請聯系部落客,定當立即删除。
感謝閱讀,公衆号 「瘦風的南牆」 ,手機端閱讀更佳,還有其他福利和心得輸出,歡迎掃碼關注🤝