進階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值, 這時怎麼辦?可以如下定義:
  • Priority.java
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中呼叫建構式比較特別,直接在列舉成員後加上括號,就可以指定建構式需要的引數,由於Enum的 ordinal()被宣告為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參數,編譯器會放在真正建構式的name與ordinal之後,真 正的 建構式會呼叫super()時傳入name與ordinal(所以你不可以在自定義建構式中呼叫super()),接著才是自定義建構式中的程式碼。在 static區塊中,編譯器仍自行維護name與ordinal的值,接著才是你呼叫自定義建構式時傳入的value值。

定義列舉時還可以實作介面,例如有個介面定義如下:
  • Command.java
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:
                System.out.println("播放停止動畫");
                break;
            case RIGHT:
                System.out.println("播放向右動畫");
                break;
            case LEFT:
                System.out.println("播放向左動畫");
                break;
            case UP:
                System.out.println("播放向上動畫");
                break;
            case DOWN:
                System.out.println("播放向下動畫");
                break;
        }
    }
}

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

不過如果在實作介面,希望各列舉成員可以有不同實作,例如上面程式片段中,其實你想讓列舉成員不僅有各自列舉實例,還可以帶有各自的可執行指令,也就 是希望可以如下執行程式:
  • Game.java
package cc.openhome;

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

希望可以有以下的執行結果:
播 放右轉動畫
播放向下動畫

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

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

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

實際上,編譯器會將Action標示為抽象類別:
public abstract class Action extends Enum implements Command {
    ...
}

並為每個列舉成員後的{}語法,產生匿名內部類別,這個匿名內部類別繼承了Action,實作了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");
            }
        };
         ...略
    }
...略

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

public enum Priority {
    MAX(10) {
        public String toString() {
            return String.format("(%2d) - 最大權限", value);
        }
    },
    NORM(5) {
        public String toString() {
            return String.format("(%2d) - 普通權限", value);
        }
    },
    MIN(1) {
        public String toString() {
            return String.format("(%2d) - 最小權限", value);
        }
    };
   
    protected 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.println(priority);
        }
    }
}   

執行結果如下:
(10) - 最大權限
( 5) - 普通權限
( 1) - 最小權限