Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
456 lines (355 loc) · 19.4 KB

File metadata and controls

456 lines (355 loc) · 19.4 KB
Copy raw file
Download raw file
Outline
Edit and raw actions

第 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

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

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

public void someMethod() {
    ....
    doAction(ActionConstants.TURN_RIGHT);
    ....
}
public void doAction(int action) { 
    switch(action) { 
        case ActionConstants.TURN_LEFT: 
            System.out.println("向左轉"); 
            break; 
        case ActionConstants.TURN_RIGHT: 
            System.out.println("向右轉"); 
            break; 
        case ActionConstants.SHOOT: 
            System.out.println("射擊"); 
            break; 
    } 
}

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

範例 11.2 CommandTool.java

public class CommandTool {
    public static final String ADMIN = "onlyfun.caterpillar.admin";
    public static final String DEVELOPER = "onlyfun.caterpillar.developer";

    public void someMethod() {
        // ....
    }
}

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

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

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

11.1.2 列舉型態入門

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

public void someMethod() {
     ....
    doAction(ActionConstants.TURN_RIGHT);
    ....
}
public void doAction(int action) { 
    switch(action) { 
        case ActionConstants.TURN_LEFT: 
            System.out.println("向左轉"); 
            break; 
        .. 
    } 
}

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

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

範例 11.3 Action.java

public enum Action {
    TURN_LEFT, 
    TURN_RIGHT, 
    SHOOT
}

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

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

範例 11.4 EnumDemo.java

public class EnumDemo {
    public static void main(String[] args) {
        doAction(Action.TURN_RIGHT);
    }
 
    public static void doAction(Action action) {
        switch(action) { 
            case TURN_LEFT: 
                System.out.println("向左轉"); 
                break; 
            case TURN_RIGHT: 
                System.out.println("向右轉"); 
                break; 
            case SHOOT: 
                System.out.println("射擊"); 
                break; 
        } 
    }
}

執行結果:

向右轉

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

...
    public static void doAction(Action action) {
        switch(action) { 
            case TURN_LEFT: 
                System.out.println("向左轉"); 
                break; 
            case TURN_RIGHT: 
                System.out.println("向右轉"); 
                break; 
            case SHOOT: 
                System.out.println("射擊"); 
                break; 
            case STOP: // Action中沒有列舉這個值
                System.out.println("停止"); 
                break;
        } 
    } ...

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

unqualified enumeration constant name required 
case STOP:
^

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

範例 11.5 EnumDemo2.java

public class EnumDemo2 {
    private enum InnerAction {TURN_LEFT, TURN_RIGHT, SHOOT};

    public static void main(String[] args) {
        doAction(InnerAction.TURN_RIGHT);
    }
 
    public static void doAction(InnerAction action) {
        switch(action) { 
            case TURN_LEFT: 
                System.out.println("向左轉"); 
                break; 
            case TURN_RIGHT: 
                System.out.println("向右轉"); 
                break; 
            case SHOOT: 
                System.out.println("射擊"); 
                break; 
        } 
    }
}

執行結果:

向右轉

由於列舉型態本質上還是個類別,所以範例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

public class ShowEnum {
    public static void main(String[] args) {
        for(Action action: Action.values()) {
            System.out.println(action.toString());
        }
    }
} 

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

TURN_LEFT
TURN_RIGHT
SHOOT

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

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

範例 11.7 EnumCompareTo.java

public class EnumCompareTo {
    public static void main(String[] args) {
        compareToAction(Action.valueOf(args[0]));
    }
 
    public static void compareToAction(Action inputAction) {
        System.out.println("輸入:" + inputAction);
        for(Action action: Action.values()) {
            System.out.println(action.compareTo(inputAction));
        }
    }
} 

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

java EnumCompareTo SHOOT
輸入:SHOOT
-2
-1
0

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

範例 11.8 EnumIndex.java

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

執行結果:

0 TURN_LEFT
1 TURN_RIGHT
2 SHOOT

11.2.2 列舉上的方法

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

範例 11.9 DetailAction.java

public enum DetailAction {
    TURN_LEFT, TURN_RIGHT, SHOOT;
 
    public String getDescription() {
        switch(this.ordinal()) {
            case 0:
                return "向左轉";
            case 1:
                return "向右轉";
            case 2:
                return "射擊";
            default:
                return null;
        }
    }
} 

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

範例 11.10 DetailActionDemo.java

public class DetailActionDemo {
    public static void main(String[] args) {
        for(DetailAction action : DetailAction.values()) {
            System.out.printf("%s:%s%n", 
                           action, action.getDescription());
        }
    }
} 

執行結果:

TURN_LEFT:向左轉
TURN_RIGHT:向右轉
SHOOT:射擊

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

範例 11.11 DetailAction2.java

public enum DetailAction2 {
    TURN_LEFT("向左轉"), TURN_RIGHT("向右轉"), SHOOT("射擊");
 
    private String description;
 
    // 不公開的建構方法
    private DetailAction2(String description) {
        this.description = description;
    }
 
    public String getDescription() {
        return description;
    }
} 

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

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

範例 11.12 IDescription.java

public interface IDescription {
    public String getDescription();
} 

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

範例 11.13 DetailAction3.java

public enum DetailAction3 implements IDescription {
    TURN_LEFT("向左轉"), TURN_RIGHT("向右轉"), SHOOT("射擊");
 
    private String description;
 
    // 不公開的建構方法
    private DetailAction3(String description) {
        this.description = description;
    }
 
    public String getDescription() {
        return description;
    }
} 

良葛格的話匣子 非公開的建構方法最常見的例子就是「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

public enum MoreAction implements IDescription {
    TURN_LEFT {
        // 實作介面上的方法
        public String getDescription() {
            return "向左轉";
        }
    },  // 記得這邊的列舉值分隔使用 ,
 
    TURN_RIGHT {
        // 實作介面上的方法
        public String getDescription() {
            return "向右轉";
        }
    }, // 記得這邊的列舉值分隔使用 ,
 
    SHOOT {
        // 實作介面上的方法
        public String getDescription() {
            return "射擊";
        }
    }; // 記得這邊的列舉值結束使用 ;
} 

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

範例 11.15 MoreActionDemo.java

public class MoreActionDemo {
    public static void main(String[] args) {
        for(MoreAction action : MoreAction.values()) {
            System.out.printf("%s:%s%n", 
                           action, action.getDescription());
        }
    }
}

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

TURN_LEFT:向左轉
TURN_RIGHT:向右轉
SHOOT:射擊

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

範例 11.16 MoreAction2java

public enum MoreAction2 {
    TURN_LEFT {
        // 實作抽象方法
        public String getDescription() {
            return "向左轉";
        }
    },  // 記得這邊的列舉值分隔使用 ,
 
    TURN_RIGHT {
        // 實作抽象方法
        public String getDescription() {
            return "向右轉";
        }
    }, // 記得這邊的列舉值分隔使用 ,
 
    SHOOT {
        // 實作抽象方法
        public String getDescription() {
            return "射擊";
        }
    }; // 記得這邊的列舉值結束使用 ;

    // 宣告個抽象方法
    public abstract String getDescription();
}

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

11.3 接下來的主題

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

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

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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.