使用 enum 列舉

June 11, 2022

在〈介面語法細節〉談過,使用介面定義列舉常數的作法,現在已不鼓勵,建議使用 enum 語法。

enum 列舉

要定義列舉常數,使用 enum 關鍵字,直接來看範例:

package cc.openhome;

public enum Action {
    STOP, RIGHT, LEFT, UP, DOWN
}

這是使用 enum 定義列舉常數最簡單的例子,STOPRIGHTLEFTUPDOWN 常數是 Action 實例,來看看如何運用:

package cc.openhome;

import static java.lang.System.out;

public class Game {
    public static void main(String[] args) {
        play(Action.RIGHT);
        play(Action.UP);
    } 

    public static void play(Action action) {
        switch(action) {
            case STOP:  // 也就是Action.STOP
                out.println("播放停止動畫");
                break;
            case RIGHT: // 也就是Action.RIGHT
                out.println("播放向右動畫");
                break;
            case LEFT: // 也就是Action.LEFT
                out.println("播放向左動畫");
                break;
            case UP:    // 也就是Action.UP
                out.println("播放向上動畫");
                break;
            case DOWN: // 也就是Action.DOWN
                out.println("播放向下動畫");
                break;
        }
    }   
}

在這個範例中,play 方法中的 action 參數宣告為 Action 型態,只接受 Action 的實例,也就是只有 Action.STOPAction.RIGHTAction.LEFTAction.UPAction.DOWN 可以傳入,case 比對必須列舉 Action 的全部實例,編譯器在編譯時期會進行型態檢查,也就不需要特別定義 default

成員的細節

每個列舉成員都會有個名稱與 int 值,可透過 name 方法取得名稱,適用於需要使用字串代表列舉值的場合,列舉的 int 值從 0 開始,依列舉順序遞增,可透過 ordinal 方法可取得,適用於需要使用 int 代表列舉值的場合。

列舉成員重新定義了 equalshashCode,並標示為 final,實作邏輯與 ObjectequalshashCode 相同:

...
    public final boolean equals(Object other) {
        return this==other;
    }

    public final int hashCode() {
        return super.hashCode();
    }
...

列舉時可以定義方法,然而不能重新定義 equalshashCode,這是因為列舉成員,在 JVM 只會存在單一實例,編譯器會如上產生 finalequalshashCode,基於 Object 定義的 equalshashCode,來比較物件相等性。

建構式、方法與介面

定義列舉時可以自定義建構式,條件是不得公開(public)或受保護(protected),也不可於建構式中呼叫 super。 來看個實際應用,先前談過 ordinal() 的值是依照成員順序,數值由 0 開始,若這不是你要的順序呢?例如原本有個 interface 定義的列舉常數:

public interface Priority {
    int MAX = 10;
    int NORM = 5;
    int MIN = 1;
}

若現在想使用 enum 重新定義列舉,又必須與既存 API 搭配,也就是必須有個 int 值符合既存 API 的 Priority 值,這時怎麼辦?可以如下定義:

package cc.openhome;

public enum Priority {
    MAX(10), NORM(5), MIN(1); 
    
    private int value;

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

    public int value() {
        return value;
    }
    
    public static void main(String[] args) {
        for(var priority : Priority.values()) {
            System.out.printf("Priority(%s, %d)%n",
                  priority, priority.value());
        }
    }
}

在這邊建構式定義為 private,想在 enum 中呼叫建構式,只要在列舉成員後加上括號,就可以指定建構式的引數,你不能定義 nameordinal 方法,它們是由編譯器產生,因此自定義了 value 方法來傳回 int 值。執行結果如下所示:

Priority(MAX, 10)
Priority(NORM, 5)
Priority(MIN, 1)

定義列舉時還可以實作介面,例如有個介面定義如下:

package cc.openhome;

public interface Command {
    void execute();
}

若要在定義列舉時實作 Command 介面,基本方式可以如下:

public enum Action3 implements Command {
    STOP, RIGHT, LEFT, UP, DOWN;

    public void execute() {
        switch(this) {
            case STOP:
                out.println("播放停止動畫");
                break;
            case RIGHT:
                out.println("播放向右動畫");
                break;
            case LEFT:
                out.println("播放向左動畫");
                break;
            case UP:
                out.println("播放向上動畫");
                break;
            case DOWN:
                out.println("播放向下動畫");
                break;
        }
    }
}

基本上就是使用 enum 定義列舉時,使用 implements 實作介面,並實作介面定義的方法,就如同定義 class 時使用 implements 實作介面。

不過實作介面時,若希望各列舉成員可以有不同實作,例如上面程式片段中,其實是想讓列舉成員帶有各自的指令,目的是希望能如下執行程式:

package cc.openhome;

public class Game3 {
    public static void play(Action3 action) {
        action.execute();
    }
    
    public static void main(String[] args) {
        Game3.play(Action3.RIGHT);
        Game3.play(Action3.DOWN);
    }
}

並可以有以下的執行結果:

播放右轉動畫
播放向下動畫

為了這個目的,先前實作 Command 時的 execute 方法時,使用了 switch 比對列舉實例,其實可以有更好的作法,就是定義 enum 時有個特定值類別本體(Value-Specific Class Bodies)語法,直接來看如何運用此語法:

package cc.openhome;

import static java.lang.System.out;

public enum Action3 implements Command {
    STOP {
        public void execute() {
            out.println("播放停止動畫");
        }
    }, 
    RIGHT {
        public void execute() {
            out.println("播放右轉動畫");
        }
    }, 
    LEFT {
        public void execute() {
            out.println("播放左轉動畫");
        }        
    }, 
    UP {
        public void execute() {
            out.println("播放向上動畫");
        }        
    }, 
    DOWN {
        public void execute() {
            out.println("播放向下動畫");
        }        
    };
}

可以看到在列舉成員後,直接加上 {} 實作 Commandexecute 方法,這代表每個列舉實例會有不同的 execute 實作,在職責分配上,比 switch 的方式清楚許多。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter