第 11 章 列舉型態(Enumerated Types)

程式中經常會使用到一些常數,如果有些常數是共用的,在 Java 中可以定義一個類別或介面來統一管理常數,而其它物件從這些類別或介面上取用常數,如果需要修改常數則可以從這些類別或介面上直接修改,而不用更動到程式的其它部份,這種使用常數的方式在 J2SE 1.4 或之前的版本相當常見。

J2SE 5.0 中新增了「列舉型態」(Enumerated Types),您可以使用這個功能取代之前 J2SE 1.4 或之前版本定義常數的方式,除了常數設置的功能之外,列舉型態還給了您許多編譯時期的檢查功能,但別想的太複雜,列舉型態本質上還是以類別的方式存在,因而它提供了這麼多額外的功能並不奇怪。


11.1 常數設置與列舉型態

在瞭解 J2SE 5.0 中新增的「列舉型態」(Enumerated Types)功能之前,您可以先瞭解一下在 J2SE 1.4 或之前版本中,是如何定義共用常數的,如此在接觸列舉型態時,您就可以感覺它所帶來的更多好處。

11.1.1 常數設置

有時候您會需要定義一些常數供程式使用,您可以使用介面或類別來定義,例如您可以使用介面來定義操作時所需的共用常數,範例 11.1 是個簡單示範。

範例 11.1 ActionConstants.java

  1. public interface ActionConstants {
  2. public static final int TURN_LEFT = 1;
  3. public static final int TURN_RIGHT = 2;
  4. public static final int SHOT = 3;
  5. }

共用的常數通常是可以直接取用並且不可被修改的,所以您在宣告時加上 “static” 與 “final”,如此您可以在程式中直接使用像是 ActionConstants.TURN_LEFT 的名稱來取用常數值,例如:

  1. public void someMethod() {
  2. ....
  3. doAction(ActionConstants.TURN_RIGHT);
  4. ....
  5. }
  6. public void doAction(int action) {
  7. switch(action) {
  8. case ActionConstants.TURN_LEFT:
  9. System.out.println("向左轉");
  10. break;
  11. case ActionConstants.TURN_RIGHT:
  12. System.out.println("向右轉");
  13. break;
  14. case ActionConstants.SHOOT:
  15. System.out.println("射擊");
  16. break;
  17. }
  18. }

如果使用類別來宣告的話,方法也是類似,例如:

範例 11.2 CommandTool.java

  1. public class CommandTool {
  2. public static final String ADMIN = "onlyfun.caterpillar.admin";
  3. public static final String DEVELOPER = "onlyfun.caterpillar.developer";
  4. public void someMethod() {
  5. // ....
  6. }
  7. }

如果常數只是在類別內部使用的話,就宣告其為 “private” 或是 “protected” 就可以了,宣告為類別外可取用的常數,通常是與類別功能相依的常數,例如使用 CommandTool 時若會使用到與 CommandTool 功能相依的常數的話,將這些常數直接宣告在 CommandTool 類別上取用時就很方便,而使用介面所宣告的常數,則通常是整個程式或某個模組中都會共用到的常數。

對於簡單的常數設置,上面的作法已經足夠了,在 J2SE 5.0 中則新增了「列舉型態」(Enumerated Types),使用列舉型態,除了簡單的常數設定功能之外,您還可以獲得像編譯時期型態檢查等更多的好處。

良葛格的話匣子 宣告常數時,通常使用大寫字母,並可以底線來區隔每個單字以利識別,例如像TURN_LEFT這樣的名稱。

11.1.2 列舉型態入門

您已經知道可以在類別或介面中宣告常數來統一管理常數,這只是讓您存取與管理常數方便而已,來看看這個例子:

  1. public void someMethod() {
  2. ....
  3. doAction(ActionConstants.TURN_RIGHT);
  4. ....
  5. }
  6. public void doAction(int action) {
  7. switch(action) {
  8. case ActionConstants.TURN_LEFT:
  9. System.out.println("向左轉");
  10. break;
  11. ..
  12. }
  13. }

這種作法本身沒錯,只不過 doAction() 方法接受的是int型態的常數,您沒有能力阻止程式設計人員對它輸入 ActionConstants 規定外的其它常數,也沒有檢查 “switch” 中列舉的值是不是正確的值,因為參數 action 就只是 int 型態而已,當然您可以自行設計一些檢查動作,這需要一些額外的工作,如果您使用 J2SE 5.0 中新增的「列舉型態」(Enumerated Types),就可以無需花額外的功夫就輕易的解決這些問題。

在 J2SE 5.0 中要定義列舉型態是使用 “enum” 關鍵字,以下先來看看列舉型態的應用,舉個實際的例子,範例 11.3 是定義了 Action 列舉型態。

範例 11.3 Action.java

  1. public enum Action {
  2. TURN_LEFT,
  3. TURN_RIGHT,
  4. SHOOT
  5. }

不用懷疑,在 Action.java 中撰寫範例 11.3 的內容然後編譯它,雖然語法上不像是在定義類別,但列舉型態骨子裏就是一個類別,所以您編譯完成後,會產生一個 Action.class 檔案。

來看下面範例 11.4 瞭解如何使用定義好的列舉型態。

範例 11.4 EnumDemo.java

  1. public class EnumDemo {
  2. public static void main(String[] args) {
  3. doAction(Action.TURN_RIGHT);
  4. }
  5. public static void doAction(Action action) {
  6. switch(action) {
  7. case TURN_LEFT:
  8. System.out.println("向左轉");
  9. break;
  10. case TURN_RIGHT:
  11. System.out.println("向右轉");
  12. break;
  13. case SHOOT:
  14. System.out.println("射擊");
  15. break;
  16. }
  17. }
  18. }

執行結果:

  1. 向右轉

除了讓您少打一些字之外,這個範例好像沒有什麼特別的,但注意到 doAction() 參數列的型態是 Action,如果您對 doAction() 方法輸入其它型態的引數,編譯器會回報錯誤,因為 doAction() 所接受的引數必須是 Action 列舉型態。
使用列舉型態還可以作到更進一步的檢驗,如果您在 “switch” 中加入了不屬於 Action 中列舉的值,編譯器也會回報錯誤,例如:

  1. ...
  2. public static void doAction(Action action) {
  3. switch(action) {
  4. case TURN_LEFT:
  5. System.out.println("向左轉");
  6. break;
  7. case TURN_RIGHT:
  8. System.out.println("向右轉");
  9. break;
  10. case SHOOT:
  11. System.out.println("射擊");
  12. break;
  13. case STOP: // Action中沒有列舉這個值
  14. System.out.println("停止");
  15. break;
  16. }
  17. } ...

在編譯時編譯器會替您作檢查,若檢查出不屬於 Action 中的列舉值,會顯示以下的錯誤:

  1. unqualified enumeration constant name required
  2. case STOP:
  3. ^

您可以在一個獨立的檔案中宣告列舉值,或是在某個類別中宣告列舉成員,例如範例 11.5 將 Action 列舉型態宣告於 EnumDemo2 類別中。

範例 11.5 EnumDemo2.java

  1. public class EnumDemo2 {
  2. private enum InnerAction {TURN_LEFT, TURN_RIGHT, SHOOT};
  3. public static void main(String[] args) {
  4. doAction(InnerAction.TURN_RIGHT);
  5. }
  6. public static void doAction(InnerAction action) {
  7. switch(action) {
  8. case TURN_LEFT:
  9. System.out.println("向左轉");
  10. break;
  11. case TURN_RIGHT:
  12. System.out.println("向右轉");
  13. break;
  14. case SHOOT:
  15. System.out.println("射擊");
  16. break;
  17. }
  18. }
  19. }

執行結果:

  1. 向右轉

由於列舉型態本質上還是個類別,所以範例11.5的列舉宣告方式有些像在宣告「內部類別」(Inner class),在您編譯完 EnumDemo2.java 檔案後,除了 EnumDemo2.class 之外,您會有一些額外的 .class 檔案產生,在這個例子中就是 EnumDemo2\$InnerAction.class 與 EnumDemo2\$1.class,看到這兩個檔案,您就應該瞭解實際上編譯器產生了「成員內部類別」以及「匿名內部類別」(第 9 章說明過內部類別)。

11.2 定義列舉型態

就簡單的應用而言,上一個小節介紹的列舉型態入門,就比舊版本的常數設定方式多了編譯時期型態檢查的好處,然而列舉型態的功能還不止這些,這個小節中再介紹更多列舉型態的定義方式,您可以將這個小節介紹的內容當作另一種定義類別的方式,如此可以幫助您理解如何定義列舉型態。

11.2.1 深入列舉型態

定義列舉型態時其實就是在定義一個類別,只不過很多細節由編譯器幫您補齊了,所以某些程度上 “enum” 關鍵字的作用就像是 “class” 或 “interface”。

當您使用 “enum” 定義列舉型態時,實際上您所定義出來的型態是繼承自 java.lang.Enum 類別,而每個被列舉的成員其實就是您定義的列舉型態的一個實例,它們都被預設為 “final”,所以您無法改變常數名稱所設定的值,它們也是 “public” 且 “static” 的成員,所以您可以透過類別名稱直接使用它們。

舉個實際的例子,範例 11.3 定義了 Action 列舉型態,當中定義的 TURN_LEFT、TURN_RIGHT、SHOOT 都是 Action 的一個物件實例,因為是物件,所以物件上自然有一些方法可以呼叫使用,例如從 Object 繼承下來的 toString() 方法被重新定義了,可以讓您直接取得列舉值的字串描述;values () 方法可以讓您取得所有的列舉成員實例,並以陣列方式傳回,您可以使用這兩個方法來簡單的將 Action(要使用範例 11.3)的列舉成員顯示出來。

範例 11.6 ShowEnum.java

  1. public class ShowEnum {
  2. public static void main(String[] args) {
  3. for(Action action: Action.values()) {
  4. System.out.println(action.toString());
  5. }
  6. }
  7. }

基本上 println() 會自動呼叫物件 toString(),所以不寫 toString() 其實也是可以的,執行結果如下:

  1. TURN_LEFT
  2. TURN_RIGHT
  3. SHOOT

由於每一個列舉的成員都是一個物件實例,所以您可以使用 “==” 或是 equals() 方法來比較列舉物件,”==” 會比較您提供的列舉物件是不是同一個物件,而 equals() 則是實質的比較兩個列舉物件的內容是否相等,使用 equals() 時預設會根據列舉物件的字串值來比較。

靜態 valueOf() 方法可以讓您將指定的字串嘗試轉換為列舉實例,您可以使用 compareTo() 方法來比較兩個列舉物件在列舉時的順序,範例是這兩個方法的實際例子。

範例 11.7 EnumCompareTo.java

  1. public class EnumCompareTo {
  2. public static void main(String[] args) {
  3. compareToAction(Action.valueOf(args[0]));
  4. }
  5. public static void compareToAction(Action inputAction) {
  6. System.out.println("輸入:" + inputAction);
  7. for(Action action: Action.values()) {
  8. System.out.println(action.compareTo(inputAction));
  9. }
  10. }
  11. }

compareTo() 如果傳回正值,表示設定為引數的列舉物件(inputAction)其順序在比較的列舉物件(action)之前,負值表示在之後,而0則表示兩個互比列舉值的位置是相同的,執行結果如下:

  1. java EnumCompareTo SHOOT
  2. 輸入:SHOOT
  3. -2
  4. -1
  5. 0

對於每一個列舉成員,您可以使用 ordinal() 方法,依列舉順序得到位置索引,預設以 0 開始,範例 11.8 是個簡單示範。

範例 11.8 EnumIndex.java

  1. public class EnumIndex {
  2. public static void main(String[] args) {
  3. for(Action action : Action.values()) {
  4. System.out.printf("%d %s%n", action.ordinal(), action);
  5. }
  6. }
  7. }

執行結果:

  1. 0 TURN_LEFT
  2. 1 TURN_RIGHT
  3. 2 SHOOT

11.2.2 列舉上的方法

定義列舉型態基本上就像是在定義類別,定義列舉型態時您也可以定義方法,例如,您也許會想要為列舉值加上一些描述,而不是使用預設的 toString() 返回值來描述列舉值,如範例 11.9 所示。

範例 11.9 DetailAction.java

  1. public enum DetailAction {
  2. TURN_LEFT, TURN_RIGHT, SHOOT;
  3. public String getDescription() {
  4. switch(this.ordinal()) {
  5. case 0:
  6. return "向左轉";
  7. case 1:
  8. return "向右轉";
  9. case 2:
  10. return "射擊";
  11. default:
  12. return null;
  13. }
  14. }
  15. }

您可以使用範例 11.10 來測試一下所定義的方法是否有用。

範例 11.10 DetailActionDemo.java

  1. public class DetailActionDemo {
  2. public static void main(String[] args) {
  3. for(DetailAction action : DetailAction.values()) {
  4. System.out.printf("%s:%s%n",
  5. action, action.getDescription());
  6. }
  7. }
  8. }

執行結果:

  1. TURN_LEFT:向左轉
  2. TURN_RIGHT:向右轉
  3. SHOOT:射擊

列舉型態既然是類別,那麼您可以為它加上建構方法(Constructor)嗎?答案是可以的,但是不得為公開的(public)建構方法,這是為了避免粗心的程式設計人員直接對列舉型態實例化,一個不公開的建構方法可以作什麼?來看看下面範例 11.11 的實作。

範例 11.11 DetailAction2.java

  1. public enum DetailAction2 {
  2. TURN_LEFT("向左轉"), TURN_RIGHT("向右轉"), SHOOT("射擊");
  3. private String description;
  4. // 不公開的建構方法
  5. private DetailAction2(String description) {
  6. this.description = description;
  7. }
  8. public String getDescription() {
  9. return description;
  10. }
  11. }

在列舉 TURN_LEFT、TURN_RIGHT、SHOOT 成員時,您可以一併指定文字描述,這個描述會在建構列舉物件時使用,範例 11.11 中您將之設定給私用成員 description,在使用 getDescription() 時將之返回,您可以使用範例 11.10 加以修改(將DetailAction改為DetailAction2),可以得到相同的顯示結果。

在定義列舉值時也可以一併實作介面(Interface),例如先來定義一個介面。

範例 11.12 IDescription.java

  1. public interface IDescription {
  2. public String getDescription();
  3. }

您可以使用這個介面規定每個實作該介面的列舉,都必須傳回一個描述列舉值的字串,如範例 11.13 所示。

範例 11.13 DetailAction3.java

  1. public enum DetailAction3 implements IDescription {
  2. TURN_LEFT("向左轉"), TURN_RIGHT("向右轉"), SHOOT("射擊");
  3. private String description;
  4. // 不公開的建構方法
  5. private DetailAction3(String description) {
  6. this.description = description;
  7. }
  8. public String getDescription() {
  9. return description;
  10. }
  11. }

良葛格的話匣子 非公開的建構方法最常見的例子就是「Singleton 模式」的應用,當某個類別只能有一個實例時,可由類別維護唯一的實例,這時可以將建構方法設定為私用(private),取用此類別的開發人員就不能自行新增多個實例了,可以看看我網站上有關 Singleton模式的介紹:

11.2.3 因值而異的類實作(Value-Specific Class Bodies)

因值而異的類實作?原文為 Value-Specific Class Bodies,其實這個功能簡單的說,實作時像是在使用「匿名內部類別」(Anonymous inner class)(9.1.2 有介紹) 來實現「Command 模式」,它讓您可以為每個列舉值定義各自的類本體與方法(Method)實作。

先來看看其中一種實現的方式,這邊要使用範例 11.12 的 IDescription 介面,您希望每個列舉的實例實作自己的 getDescription() 方法(而不是像範例 11.13 所介紹的,在定義列舉時實作一個統一的 getDescription() 方法),如範例 11.14 所示。

範例 11.14 MoreAction.java

  1. public enum MoreAction implements IDescription {
  2. TURN_LEFT {
  3. // 實作介面上的方法
  4. public String getDescription() {
  5. return "向左轉";
  6. }
  7. }, // 記得這邊的列舉值分隔使用 ,
  8. TURN_RIGHT {
  9. // 實作介面上的方法
  10. public String getDescription() {
  11. return "向右轉";
  12. }
  13. }, // 記得這邊的列舉值分隔使用 ,
  14. SHOOT {
  15. // 實作介面上的方法
  16. public String getDescription() {
  17. return "射擊";
  18. }
  19. }; // 記得這邊的列舉值結束使用 ;
  20. }

每個列舉成員的 ‘{‘ 與 ‘}’ 之間是類本體,您還可以在當中如同定義類別一樣的宣告資料成員或實作方法。TURN_LEFT、TURN_RIGHT 與 SHOOT 三個 MoreAction 的列舉實例各自在本體(Body),也就是 ‘{‘ 與 ‘}’ 之間實作了自己的 getDescription() 方法,而不是像範例 11.13 中統一實作在 DetailAction3 中,使用範例 11.15 作個測試。

範例 11.15 MoreActionDemo.java

  1. public class MoreActionDemo {
  2. public static void main(String[] args) {
  3. for(MoreAction action : MoreAction.values()) {
  4. System.out.printf("%s:%s%n",
  5. action, action.getDescription());
  6. }
  7. }
  8. }

這個例子是將「因值而異的類實作」用在返回列舉值描述上,您可以依相同的方式,為每個列舉值加上一些各自的方法實作,而呼叫的介面是統一的,執行結果會顯示各自的列舉描述:

  1. TURN_LEFT:向左轉
  2. TURN_RIGHT:向右轉
  3. SHOOT:射擊

您也可以運用抽象方法來改寫範例 11.14,如範例 11.16 所示。

範例 11.16 MoreAction2java

  1. public enum MoreAction2 {
  2. TURN_LEFT {
  3. // 實作抽象方法
  4. public String getDescription() {
  5. return "向左轉";
  6. }
  7. }, // 記得這邊的列舉值分隔使用 ,
  8. TURN_RIGHT {
  9. // 實作抽象方法
  10. public String getDescription() {
  11. return "向右轉";
  12. }
  13. }, // 記得這邊的列舉值分隔使用 ,
  14. SHOOT {
  15. // 實作抽象方法
  16. public String getDescription() {
  17. return "射擊";
  18. }
  19. }; // 記得這邊的列舉值結束使用 ;
  20. // 宣告個抽象方法
  21. public abstract String getDescription();
  22. }

MoreAction2 與 MoreAction 不同的地方在於 MoreAction2 是實作抽象方法,您可以改寫一些範例 11.15(將 MoreAction 改為 MoreAction2),而執行結果是一樣的;基本上定義介面方法或抽象方法,是為了知道物件的操作介面,這樣您才能去操作這個物件。

11.3 接下來的主題

每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:

  • 知道如何使用類別或介面定義以管理常數
  • 會使用 “enum” 來取代常數列舉
  • 知道列舉型態實際上就是在定義一個類別

下一個章節要介紹的也是 J2SE 5.0 的新功能:泛型(Generics)。除了可以讓您少寫幾個類別的程式碼之外,泛型的目的還在讓您定義「安全的」泛型類別(Generics class),事實上 J2SE 5.0 前就用 Object 解決了泛型類別的部份需求,J2SE 5.0 之後再解決的是型態安全問題。