進階 enum 運用


觀察 瞭解 java.lang.Enum 反編譯Action列舉的程式碼,可以看到還有個values()方法,這個方法會將內部維護Action列舉實例的陣列複製後傳回,如果你想要知道有哪些列舉成員,就可以使用這個方法,由於是複製品,因此改變傳回的陣列,並不會影響Action內部所維護的陣列。

列舉型態既然繼承自Enum的類別,除了由編譯器自動產生的private建構式之外,也可以自行定義建構式,條件是不得為公開(public)建構式,也不可以於建構式中呼叫super()

來看個實際應用,先前談過ordinal的值,會是使用enum列舉的成員順序,數值由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(Priority priority : Priority.values()) {
            System.out.printf("Priority(%s, %d)%n",
                  priority, priority.value());
        }
    }
}

在這邊建構式定義為private,在enum中呼叫建構式比較特別,直接在列舉成員後加上括號,就可以指定建構式需要的引數,由於Enumordinal()被宣告為final,不能重新定義,所以自定義了value()方法來傳回int值。執行結果如下所示:

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

可以看看Priority.class反編譯後的結果:

public final class Priority extends Enum {
    ...略
    private Priority(String s, int i, int value) {
        super(s, i);
        this.value = value;
    }
    public int value() {
        return value;
    }
    ...略
    public static final Priority MAX;
    public static final Priority NORM;
    public static final Priority MIN;
    private int value;
    private static final Priority \$VALUES[];

    static
    {
        MAX = new Priority("MAX", 0, 10);
        NORM = new Priority("NORM", 1, 5);
        MIN = new Priority("MIN", 2, 1);
        \$VALUES = (new Priority[] {
            MAX, NORM, MIN
        });
    }
}
   
實際上你定義的建構式只是編譯器用來產生真正建構式時參考之用,你定義的value參數,編譯器會放在真正建構式的nameordinal之後,真正的建構式會呼叫super()時傳入nameordinal(所以你不可以在自定義建構式中呼叫super()),接著才是自定義建構式中的程式碼。在static區塊中,編譯器仍自行維護nameordinal的值,接著才是你呼叫自定義建構式時傳入的value值。

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

package cc.openhome;

public interface Command {
    void execute();
}

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

import static java.lang.System.out;

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 main(String[] args) {
        Game3.play(Action3.RIGHT);
        Game3.play(Action3.DOWN);
    }

    static void play(Action3 action) {
        action.execute();
    }
}

你希望可以有以下的執行結果:

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

為了這個目的,先前實作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的方式清楚許多。

實際上,編譯器會將Action3標示為抽象類別:

public abstract class Action3 extends Enum implements Command {
    ...
}

並為每個列舉成員後的{}語法,產生匿名內部類別,這個匿名內部類別繼承了Action3,實作了execute()方法:

...略
    static
    {
        STOP = new Action3("STOP", 0) {
            public void execute() {
                System.out.println("\u64AD\u653E\u505C\u6B62\u52D5\u756B");
            }
        };
        RIGHT = new Action3("STOP", 0) {
            public void execute() {
                System.out.println("\u64AD\u653E\u505C\u6B62\u52D5\u756B");
            }
        };
         ...略
    }
...略

所以每個列舉成員,實際上都參考至Action3的匿名子類別。瞭解這個原理後,也就可以知道,特定值類別本體語法不僅在實作介面時可以使用,也可以運用在重新定義父類別方法。例如重新定義toString(),以先前Priority為例,可改寫為以下:

package cc.openhome;

import static java.lang.String.format;

public enum Priority2 {
    MAX(10) {
        public String toString() {
            return format("(%2d) - 最大權限", value);
        }
    }, 
    NORM(5) {
        public String toString() {
            return format("(%2d) - 普通權限", value);
        }
    }, 
    MIN(1) {
        public String toString() {
            return format("(%2d) - 最小權限", value);
        }
    };
    
    protected int value;

    private Priority2(int value) {
        this.value = value;
    }
    public int value() {
        return value;
    }
    
    public static void main(String[] args) {
        for(Priority2 priority : Priority2.values()) {
            System.out.println(priority);
        }
    }
} 

執行結果如下:

(10) - 最大權限
( 5) - 普通權限
( 1) - 最小權限