Java 枚舉 enum 詳解

本文部分摘自 On Java 8

枚舉類型

Java5 中添加了一個 enum 關鍵字,通過 enum 關鍵字,我們可以將一組擁有具名的值的有限集合創建為一種新的類型,這些具名的值可以作為常規的程序組件使用,例如:

public enum Spiciness {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

這裡創建了一個名為 Spiciness 的枚舉類型,它有 5 個值。由於枚舉類型的實例是常量,因此按照命名慣例,它們都用大寫字母表示(如果名稱中含有多個單詞,使用下劃線分隔)

要使用 enum,需要創建一個該類型的引用,然後將其賦值給某個實例:

public class SimpleEnumUse {
    public static void main(String[] args) {
        Spiciness howHot = Spiciness.MEDIUM;
        System.out.println(howHot);
    }
}
// 輸出:MEDIUM

在 switch 中使用 enum,是 enum 提供的一項非常便利的功能。一般來說,在 switch 中只能使用整數值,而枚舉實例天生就具備整數值的次序,並且可以通過 ordinal() 方法取得其次序,因此我們可以在 switch 語句中使用 enum

一般情況下我們必須使用 enum 類型來修飾一個 enum 實例,但是在 case 語句中卻不必如此。下面的例子使用 enum 構造了一個模擬紅綠燈狀態變化:

enum Signal { GREEN, YELLOW, RED, }

public class TrafficLight {
    
    Signal color = Signal.RED;
    
    public void change() {
        switch(color) {
            case RED: color = Signal.GREEN;
                break;
            case GREEN: color = Signal.YELLOW;
                break;
            case YELLOW: color = Signal.RED;
                break;
        }
    }
    
    @Override
    public String toString() {
        return "The traffic light is " + color;
    }
    
    public static void main(String[] args) {
        TrafficLight t = new TrafficLight();
        for(int i = 0; i < 7; i++) {
            System.out.println(t);
            t.change();
        }
    }
}

枚舉的基本特性

Java 中的每一個枚舉都繼承自 java.lang.Enum 類,所有枚舉實例都可以調用 Enum 類的方法

調用 enum 的 values() 方法,返回 enum 實例的數組,而且該數組中的元素嚴格保持其在 enum 中聲明時的順序,因此你可以在循環中使用 values() 返回的數組

enum Shrubbery { GROUND, CRAWLING, HANGING }
public class EnumClass {
    public static void main(String[] args) {
        for(Shrubbery s : Shrubbery.values()) {
            System.out.println(s);
            // 返回每個枚舉實例在聲明時的次序
            System.out.println(s.ordinal());
            // 返回與此枚舉常量的枚舉類型相對應的 Class 對象
            System.out.println(s.getDeclaringClass());
            // 返回枚舉實例聲明時的名字,效果等同於直接打印
            System.out.println(s.name());
            ...
        }
    }
}
// 輸出:
// GROUND
// 0
// GROUND
// CRAWLING
// 1
// CRAWLING
// HANGING
// 2
// HANGING

可以使用 == 來比較 enum 實例,編譯器會自動為你提供 equals() 和 hashCode() 方法。同時,Enum 類實現了 Comparable 接口,所以它具有 compareTo() 方法,同時,它還實現了 Serializable 接口

ValueOf() 方法是在 Enum 中定義的 static 方法,根據給定的名字返回相應的 enum 實例,如果不存在給定名字的實例,將拋出異常

Shrubbery shrub = Enum.valueOf(Shrubbery.class, "HANGING");

我們再來看看 values() 方法,為什麼要說這個呢?前面提到,編譯器為你創建的 enum 類都繼承自 Enum 類。然而,如果你研究一下 Enum 類就會發現,它並沒有 values() 方法。可我們明明已經用過該方法了呀,難道是這個方法被藏起來了?答案是,values() 是由編譯器添加的 static 方法,編譯器還會為創建的枚舉類標記為 static final,所以無法被繼承

由於 values() 方法是由編譯器插入到 enum 定義中的 static 方法,所以,如果你將 enum 實例向上轉型為 Enum,那麼 values() 方法就不可用了。不過,在 Class 中有一個 getEnumConstants() 方法,所以即便 Enum 接口中沒有 values() 方法,我們仍然可以通過 Class 對象取得所有 enum 實例

enum Search { HITHER, YON }
public class UpcastEnum {
    public static void main(String[] args) {
        for(Enum en : e.getClass().getEnumConstants())
            System.out.println(en);
    }
}

因為 getEnumConstants() 是 Class 上的方法,所以你甚至可以對不是枚舉的類調用此方法,只不過,此時該方法返回 null

方法添加

除了不能繼承自一個 enum 之外,我們基本上可以將 enum 看作一個常規的類。也就是說我們可以向 enum 中添加方法。enum 甚至可以有 main() 方法

我們希望每個枚舉實例能夠返回對自身的描述,而不僅僅只是默認的 toString() 實現,這隻能返回枚舉實例的名字。為此,你可以提供一個構造器,專門負責處理這個額外的信息,然後添加一個方法,返回這個描述信息。看一看下面的示例:

public enum OzWitch {
    
    WEST("Miss Gulch, aka the Wicked Witch of the West"),
    NORTH("Glinda, the Good Witch of the North"),
    EAST("Wicked Witch of the East, wearer of the Ruby "),
    SOUTH("Good by inference, but missing");	// 必須在 enum 實例序列的最後添加一個分號
    
    private String description;
    
    private OzWitch(String description) {
        this.description = description;
    }
    
    public String getDescription() { return description; }
    
    public static void main(String[] args) {
        for(OzWitch witch : OzWitch.values())
            System.out.println(witch + ": " + witch.getDescription());
    }
}

在這個例子中,我們有意識地將 enum 的構造器聲明為 private,但對於它的可訪問性而言,並沒有什麼變化,因為(即使不聲明為 private)我們只能在 enum 定義的內部使用其構造器創建 enum 實例。一旦 enum 的定義結束,編譯器就不允許我們再使用其構造器來創建任何實例了

如果希望覆蓋 enum 中的方法,例如覆蓋 toString() 方法,與覆蓋一般類的方法沒有區別:

public enum SpaceShip {
    
    SCOUT, CARGO, TRANSPORT,
    CRUISER, BATTLESHIP, MOTHERSHIP;
    
    @Override
    public String toString() {
        String id = name();
        String lower = id.substring(1).toLowerCase();
        return id.charAt(0) + lower;
    }
    
    public static void main(String[] args) {
        Stream.of(values()).forEach(System.out::println);
    }
}

實現接口

我們已經知道,所有的 enum 都繼承自 Java.lang.Enum 類。由於 Java 不支持多重繼承,所以 enum 不能再繼承其他類,但可以實現一個或多個接口

enum CartoonCharacter implements Supplier<CartoonCharacter> {
    
    SLAPPY, SPANKY, PUNCHY,
    SILLY, BOUNCY, NUTTY, BOB;
    
    private Random rand = new Random(47);
    
    @Override
    public CartoonCharacter get() {
        return values()[rand.nextInt(values().length)];
    }
}

通過實現一個供給型接口,就可以通過調用 get() 方法得到一個隨機的枚舉值。我們可以藉助泛型,使隨機選擇這個工作更加一般化,成為一個通用的工具類

public class Enums {
    
    private static Random rand = new Random(47);

    public static <T extends Enum<T>> T random(Class<T> ec) {
        return random(ec.getEnumConstants());
    }

    public static <T> T random(T[] values) {
        return values[rand.nextInt(values.length)];
    }
}

<T extends Enum> 表示 T 是一個 enum 實例。而將 Class 作為參數的話,我們就可以利用 Class 對象得到 enum 實例的數組了。重載後的 random() 方法只需使用 T[] 作為參數,因為它並不會調用 Enum 上的任何操作,它只需從數組中隨機選擇一個元素即可。這樣,最終的返回類型正是 enum 的類型

使用接口可以幫助我們實現將枚舉元素分類的目的,舉例來說,假設你想用 enum 來表示不同類別的食物,同時還希望每個 enum 元素仍然保持 Food 類型,那可以這樣實現:

public interface Food {
    
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS;
    }
    
    enum MainCourse implements Food {
        LASAGNE, BURRITO, PAD_THAI,LENTILS, HUMMOUS, VINDALOO;
    }
    
    enum Dessert implements Food {
        TIRAMISU, GELATO, BLACK_FOREST_CAKE,FRUIT, CREME_CARAMEL;
    }
    
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,LATTE, CAPPUCCINO, TEA, HERB_TEA;
    }
}

對於 enum 而言,實現接口是使其子類化的唯一辦法,所以嵌入在 Food 中的每個 enum 都實現了 Food 接口。現在,在下面的程序中,我們可以說「所有東西都是某種類型的 Food”

public class TypeOfFood {
    
    public static void main(String[] args) {
        
        Food food = Appetizer.SALAD;
        food = MainCourse.LASAGNE;
        food = Dessert.GELATO;
        food = Coffee.CAPPUCCINO;
    }
}

如果 enum 的數量太多,那麼一個接口中的代碼量可能就很大。我們可以利用 getEnumConstants() 方法,根據某個 Class 對象取得某個 Food 子類的所有 enum 實例

public enum Course {
    
    APPETIZER(Food.Appetizer.class),
    MAINCOURSE(Food.MainCourse.class),
    DESSERT(Food.Dessert.class),
    COFFEE(Food.Coffee.class);
    
    private Food[] values;
    
    private Course(Class<? extends Food> kind) {
        values = kind.getEnumConstants();
    }
    
    // 隨機獲取 Food 子類的某個 enum 實例
    public Food randomSelection() {
        return Enums.random(values);
    }
}

如果對內部類熟悉的話,我們還可以使用更加簡潔的管理枚舉的方式,就是將第一種方式中的接口嵌套在枚舉里,使得代碼具有更清晰的結構

enum SecurityCategory {
    
    STOCK(Security.Stock.class),
    BOND(Security.Bond.class);
    
    Security[] values;
    
    SecurityCategory(Class<? extends Security> kind) {
        values = kind.getEnumConstants();
    }
    
    interface Security {
        enum Stock implements Security {
            SHORT, LONG, MARGIN
        }
        enum Bond implements Security {
            MUNICIPAL, JUNK
        }
    }
    
    public Security randomSelection() {
        return Enums.random(values);
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            SecurityCategory category = Enums.random(SecurityCategory.class);
            System.out.println(category + ": " + category.randomSelection());
        }
    }
}

EnumSet

EnumSet 是一個用來操作 Enum 的集合,可以用來存放屬於同一枚舉類型的枚舉常量,其中元素存放的次序決定於 enum 實例定義時的次序。EnumSet 的設計初衷是為了替代傳統的基於 int 的「位標誌」。傳統的「位標誌」可以用來表示某種「開/關」信息,不過,使用這種標誌,我們最終操作的只是一些 bit,而不是這些 bit 想要表達的概念,因此很容易寫出令人費解的代碼

既然 EnumSet 要替代 bit 標誌,那麼它的性能應該要做到與使用 bit 一樣高效才對。EnumSet 的基礎是 long,一個 long 值有 64 位,一個 enum 實例只需一位 bit 表示其是否存在。 也就是說,在不超過一個 long 的表達能力的情況下,你的 EnumSet 可以應用於最多不超過 64 個元素的 enum。如果 enum 超過了 64 個元素,EnumSet 會在必要時增加一個 long

EnumSet 的方法如下:

方法 作用
allOf(Class elementType) 創建一個包含指定枚舉類里所有枚舉值的 EnumSet 集合
complementOf(EnumSet e) 創建一個元素類型與指定 EnumSet 里元素類型相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的的枚舉值
copyOf(Collection c) 使用一個普通集合來創建 EnumSet 集合
copyOf(EnumSet e) 創建一個指定 EnumSet 具有相同元素類型、相同集合元素的 EnumSet 集合
noneOf(Class elementType) 創建一個元素類型為指定枚舉類型的空 EnumSet
of(E first,E…rest) 創建一個包含一個或多個枚舉值的 EnumSet 集合,傳入的多個枚舉值必須屬於同一個枚舉類
range(E from,E to) 創建一個包含從 from 枚舉值到 to 枚舉值範圍內所有枚舉值的 EnumSet 集合

示例代碼:

public enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}

public class EnumSets {
    public static void main(String[] args) {
        EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);
        points.add(BATHROOM);
        System.out.println(points);
        points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);
        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);
        points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
        System.out.println(points);
        points = EnumSet.complementOf(points);
        System.out.println(points);
    }
}

EnumMap

EnumMap 是一種特殊的 Map,它要求 key 必須為枚舉類型,value 沒有限制,底層由雙數組實現(一個存放 key,另一個存放 value),同時,當 value 為 null 時會特殊處理為一個 Object 對象,和 EnumSet 一樣,元素存放的次序決定於 enum 實例定義時的次序

// key 類型
private final Class<K> keyType;

// key 數組
private transient K[] keyUniverse;

// value 數組
private transient Object[] vals;

// 鍵值對個數
private transient int size = 0;

// value 為 null 時對應的值
private static final Object NULL = new Object() {
    public int hashCode() {
        return 0;
    }
    public String toString() {
        return "java.util.EnumMap.NULL";
    }
};

由於 EnumMap 可以存放枚舉類型,所以初始化時必須指定枚舉類型,EnumMap 提供了三個構造函數

// 使用指定的鍵類型創建一個空的枚舉映射
EnumMap(Class<K> keyType);
// 創建與指定的枚舉映射相同的鍵類型的枚舉映射,最初包含相同的映射(如果有)
EnumMap(EnumMap<K,? extends V> m);
// 創建從指定映射初始化的枚舉映射
EnumMap(Map<K,? extends V> m);

除此之外,EnumMap 與普通 Map 在操作上沒有區別,EnumMap 的優點在於允許程序員改變值對象,而常量相關的方法在編譯期就被固定了

常量特定方法

我們可以為 enum 定義一個或多個 abstract 方法,然後為每個 enum 實例實現該抽象方法

public enum ConstantSpecificMethod {
    DATE_TIME {
        @Override
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }
    },
    CLASSPATH {
        @Override
        String getInfo() {
            return System.getenv("CLASSPATH");
        }
    },
    VERSION {
        @Override
        String getInfo() {
            return System.getProperty("java.version");
        }
    };
    abstract String getInfo();
    public static void main(String[] args) {
        for(ConstantSpecificMethod csm : values())
            System.out.println(csm.getInfo());
    }
}

在面向對象的程序設計中,不同的行為與不同的類關聯。通過常量相關的方法,每個 enum 實例可以具備自己獨特的行為,這似乎說明每個 enum 實例就像一個獨特的類。在上面的例子中,enum 實例似乎被當作其超類 ConstantSpecificMethod 來使用,在調用 getInfo() 方法時,體現出多態的行為

然而,enum 實例與類的相似之處也僅限於此了。我們並不能真的將 enum 實例作為一個類型來使用,因為每一個 enum 元素都是指定枚舉類型的 static final 實例

除了 abstract 方法外,程序員還可以覆蓋普通方法

public enum OverrideConstantSpecific {
    NUT, BOLT,
    WASHER {
        @Override
        void f() {
            System.out.println("Overridden method");
        }
    };
    void f() {
        System.out.println("default behavior");
    }
    public static void main(String[] args) {
        for(OverrideConstantSpecific ocs : values()) {
            System.out.print(ocs + ": ");
            ocs.f();
        }
    }
}

多路分發

現在有一個數學表達式 Number.plus(Number),我們知道 Number 是各種數字對象的超類,假設有 a 和 b 兩個 Number 類型的對象,根據上述的表達式代入得 a.plus(b),但你現在只知道 a、b 屬於 Number 類型,具體是什麼數字你並不知道,有可能是整數、浮點數,根據不同的數字類型,執行數學操作後的結果應該也不一樣才對,怎麼讓它們正確地交互呢?

如果你了解 Java 多態就知道,Java 多態的實現依賴的是 Java 的動態綁定機制,在運行時發現對象的真實類型。但 Java 只支持單路分發,就是說如果要執行的操作包含不止一個類型未知的對象,那麼 Java 的動態綁定機制只能處理其中一個類型,a.plus(b) 涉及到兩個類型,自然無法解決我們的問題,所以我們必須自己來判定其他類型

解決問題的方法就是多路分發,上面的例子,由於只有兩個分發,一般稱為兩路分發。多態只能發生在方法調用時,所以,如果你想使用兩路分發,那麼就必須有兩個方法調用:第一個方法調用決定第一個未知類型,第二個方法調用決定第二個未知的類型。程序員必須設定好某種配置,以便一個方法調用能夠引出其他方法調用,從而在這個過程中處理多種類型

來看一個猜拳的例子:

package enums;
public enum Outcome { WIN, LOSE, DRAW }	// 猜拳的結果:勝、負、平手
package enums;
import java.util.*;
import static enums.Outcome.*;	// 引入 enums,這樣就不用寫前綴 Outcome 了
interface Item {
    Outcome compete(Item it);
    Outcome eval(Paper p);
    Outcome eval(Scissors s);
    Outcome eval(Rock r);
}
class Paper implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }
    @Override
    public Outcome eval(Paper p) { return DRAW; }
    @Override
    public Outcome eval(Scissors s) { return WIN; }
    @Override
    public Outcome eval(Rock r) { return LOSE; }
    @Override
    public String toString() { return "Paper"; }
}
class Scissors implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }
    @Override
    public Outcome eval(Paper p) { return LOSE; }
    @Override
    public Outcome eval(Scissors s) { return DRAW; }
    @Override
    public Outcome eval(Rock r) { return WIN; }
    @Override
    public String toString() { return "Scissors"; }
}
class Rock implements Item {
    @Override
    public Outcome compete(Item it) {
        return it.eval(this);
    }
    @Override
    public Outcome eval(Paper p) { return WIN; }
    @Override
    public Outcome eval(Scissors s) { return LOSE; }
    @Override
    public Outcome eval(Rock r) { return DRAW; }
    @Override
    public String toString() { return "Rock"; }
}
public class RoShamBo1 {
    static final int SIZE = 20;
    private static Random rand = new Random(47);
    public static Item newItem() {
        switch(rand.nextInt(3)) {
            default:
            case 0: return new Scissors();
            case 1: return new Paper();
            case 2: return new Rock();
        }
    }
    public static void match(Item a, Item b) {
        System.out.println(
                a + " vs. " + b + ": " + a.compete(b));
    }
    public static void main(String[] args) {
        for(int i = 0; i < SIZE; i++)
            match(newItem(), newItem());
    }
}

上面就是多路分發的實現,它的好處在於避免判定多個對象的類型的冗餘代碼,不過配置過程需要很多道工序。我們既然學習了枚舉,自然可以考慮用枚舉對代碼進行優化

public interface Competitor<T extends Competitor<T>> {
    Outcome compete(T competitor);
}

public class RoShamBo {
    
    public static <T extends Competitor<T>> void match(T a, T b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }
    
    public static <T extends Enum<T> & Competitor<T>> 
        void play(Class<T> rsbClass, int size) {
        for(int i = 0; i < size; i++)
            match(Enums.random(rsbClass),Enums.random(rsbClass));
    }
}

public enum RoShamBo2 implements Competitor<RoShamBo2> {
    
    PAPER(DRAW, LOSE, WIN),
    SCISSORS(WIN, DRAW, LOSE),
    ROCK(LOSE, WIN, DRAW);
    
    private Outcome vPAPER, vSCISSORS, vROCK;
    
    RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
        this.vPAPER = paper;
        this.vSCISSORS = scissors;
        this.vROCK = rock;
    }
    
    @Override
    public Outcome compete(RoShamBo2 it) {
        switch(it) {
            default:
            case PAPER: return vPAPER;
            case SCISSORS: return vSCISSORS;
            case ROCK: return vROCK;
        }
    }
    
    public static void main(String[] args) {
        RoShamBo.play(RoShamBo2.class, 20);
    }
}

也可以使用將 enum 用在 switch 語句中

import static enums.Outcome.*;
public enum RoShamBo3 implements Competitor<RoShamBo3> {
    PAPER {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch(it) {
                default:
                case PAPER: return DRAW;
                case SCISSORS: return LOSE;
                case ROCK: return WIN;
            }
        }
    },
    SCISSORS {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch(it) {
                default:
                case PAPER: return WIN;
                case SCISSORS: return DRAW;
                case ROCK: return LOSE;
            }
        }
    },
    ROCK {
        @Override
        public Outcome compete(RoShamBo3 it) {
            switch(it) {
                default:
                case PAPER: return LOSE;
                case SCISSORS: return WIN;
                case ROCK: return DRAW;
            }
        }
    };
    @Override
    public abstract Outcome compete(RoShamBo3 it);
    public static void main(String[] args) {
        RoShamBo.play(RoShamBo3.class, 20);
    }
}

對上述代碼還可以再壓縮一下

public enum RoShamBo4 implements Competitor<RoShamBo4> {
    ROCK {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(SCISSORS, opponent);
        }
    },
    SCISSORS {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(PAPER, opponent);
        }
    },
    PAPER {
        @Override
        public Outcome compete(RoShamBo4 opponent) {
            return compete(ROCK, opponent);
        }
    };
    Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) {
        return ((opponent == this) ? Outcome.DRAW
                : ((opponent == loser) ? Outcome.WIN
                : Outcome.LOSE));
    }
    public static void main(String[] args) {
        RoShamBo.play(RoShamBo4.class, 20);
    }
}

使用 EnumMap 能夠實現真正的兩路分發。EnumMap 是為 enum 專門設計的一種性能非常好的特殊 Map。由於我們的目的是摸索出兩種未知的類型,所以可以用一個 EnumMap 的 EnumMap 來實現兩路分發:

package enums;
import java.util.*;
import static enums.Outcome.*;
enum RoShamBo5 implements Competitor<RoShamBo5> {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo5,EnumMap<RoShamBo5,Outcome>>
            table = new EnumMap<>(RoShamBo5.class);
    static {
        for(RoShamBo5 it : RoShamBo5.values())
            table.put(it, new EnumMap<>(RoShamBo5.class));
        initRow(PAPER, DRAW, LOSE, WIN);
        initRow(SCISSORS, WIN, DRAW, LOSE);
        initRow(ROCK, LOSE, WIN, DRAW);
    }
    static void initRow(RoShamBo5 it,
                        Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
        EnumMap<RoShamBo5,Outcome> row =
                RoShamBo5.table.get(it);
        row.put(RoShamBo5.PAPER, vPAPER);
        row.put(RoShamBo5.SCISSORS, vSCISSORS);
        row.put(RoShamBo5.ROCK, vROCK);
    }
    @Override
    public Outcome compete(RoShamBo5 it) {
        return table.get(this).get(it);
    }
    public static void main(String[] args) {
        RoShamBo.play(RoShamBo5.class, 20);
    }
}

我們還可以進一步簡化實現兩路分發的解決方案。我們注意到,每個 enum 實例都有一個固定的值(基於其聲明的次序),並且可以通過 ordinal() 方法取得該值。因此我們可以使用二維數組,將競爭者映射到競爭結果。採用這種方式能夠獲得最簡潔、最直接的解決方案

package enums;
import static enums.Outcome.*;
enum RoShamBo6 implements Competitor<RoShamBo6> {
    PAPER, SCISSORS, ROCK;
    private static Outcome[][] table = {
            { DRAW, LOSE, WIN }, // PAPER
            { WIN, DRAW, LOSE }, // SCISSORS
            { LOSE, WIN, DRAW }, // ROCK
    };
    @Override
    public Outcome compete(RoShamBo6 other) {
        return table[this.ordinal()][other.ordinal()];
    }
    public static void main(String[] args) {
        RoShamBo.play(RoShamBo6.class, 20);
    }
}

Tags: