1. 程式人生 > >Effective Java 第三版——38. 使用接口模擬可擴展的枚舉

Effective Java 第三版——38. 使用接口模擬可擴展的枚舉

rem 第一時間 輔助類 [] 接口類 img IT 基本類 value

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這裏第一時間翻譯成中文版。供大家學習分享之用。

技術分享圖片

38. 使用接口模擬可擴展的枚舉

在幾乎所有方面,枚舉類型都優於本書第一版中描述的類型安全模式[Bloch01]。 從表面上看,一個例外涉及可擴展性,這在原始模式下是可能的,但不受語言結構支持。 換句話說,使用該模式,有可能使一個枚舉類型擴展為另一個; 使用語言功能特性,它不能這樣做。 這不是偶然的。 大多數情況下,枚舉的可擴展性是一個糟糕的主意。 令人困惑的是,擴展類型的元素是基類型的實例,反之亦然。 枚舉基本類型及其擴展的所有元素沒有好的方法。 最後,可擴展性會使設計和實現的很多方面復雜化。

也就是說,對於可擴展枚舉類型至少有一個有說服力的用例,這就是操作碼( operation codes),也稱為opcodes。 操作碼是枚舉類型,其元素表示某些機器上的操作,例如條目 34中的Operation類型,它表示簡單計算器上的功能。 有時需要讓API的用戶提供他們自己的操作,從而有效地擴展API提供的操作集。

幸運的是,使用枚舉類型有一個很好的方法來實現這種效果。基本思想是利用枚舉類型可以通過為opcode類型定義一個接口,並實現任意接口。例如,這裏是來自條目 34的Operation類型的可擴展版本:

// Emulated extensible enum using an interface
public interface Operation {
    double apply(double x, double y);
}


public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };
    private final String symbol;


    BasicOperation(String symbol) {
        this.symbol = symbol;
    }


    @Override public String toString() {
        return symbol;
    }
}

雖然枚舉類型(BasicOperation)不可擴展,但接口類型(Operation)是可以擴展的,並且它是用於表示API中的操作的接口類型。 你可以定義另一個實現此接口的枚舉類型,並使用此新類型的實例來代替基本類型。 例如,假設想要定義前面所示的操作類型的擴展,包括指數運算和余數運算。 你所要做的就是編寫一個實現Operation接口的枚舉類型:

// Emulated extension enum
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private final String symbol;

    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }

    @Override public String toString() {
        return symbol;
    }
}

只要API編寫為接口類型(Operation),而不是實現(BasicOperation),現在就可以在任何可以使用基本操作的地方使用新操作。請註意,不必在枚舉中聲明apply抽象方法,就像您在具有實例特定方法實現的非擴展枚舉中所做的那樣(第162頁)。 這是因為抽象方法(apply)是接口(Operation)的成員。

不僅可以在任何需要“基本枚舉”的地方傳遞“擴展枚舉”的單個實例,而且還可以傳入整個擴展枚舉類型,並使用其元素。 例如,這裏是第163頁上的一個測試程序版本,它執行之前定義的所有擴展操作:

public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(ExtendedOperation.class, x, y);
}


private static <T extends Enum<T> & Operation> void test(
        Class<T> opEnumType, double x, double y) {
    for (Operation op : opEnumType.getEnumConstants())
        System.out.printf("%f %s %f = %f%n",
                          x, op, y, op.apply(x, y));
}

註意,擴展的操作類型的類字面文字(ExtendedOperation.class)從main方法裏傳遞給了test方法,用來描述擴展操作的集合。這個類的字面文字用作限定的類型令牌(條目 33)。opEnumType參數中復雜的聲明(<T extends Enum<T> & Operation> Class<T>)確保了Class對象既是枚舉又是Operation的子類,這正是遍歷元素和執行每個元素相關聯的操作時所需要的。

第二種方式是傳遞一個Collection<? extends Operation>,這是一個限定通配符類型(條目 31),而不是傳遞了一個class對象:

public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(Arrays.asList(ExtendedOperation.values()), x, y);
}

private static void test(Collection<? extends Operation> opSet,
        double x, double y) {
    for (Operation op : opSet)
        System.out.printf("%f %s %f = %f%n",
                          x, op, y, op.apply(x, y));
}

生成的代碼稍微不那麽復雜,test方法靈活一點:它允許調用者將多個實現類型的操作組合在一起。另一方面,也放棄了在指定操作上使用EnumSet(條目 36)和EnumMap(條目 37)的能力。

上面的兩個程序在運行命令行輸入參數4和2時生成以下輸出:

4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000

使用接口來模擬可擴展枚舉的一個小缺點是,實現不能從一個枚舉類型繼承到另一個枚舉類型。如果實現代碼不依賴於任何狀態,則可以使用默認實現(條目 20)將其放置在接口中。在我們的Operation示例中,存儲和檢索與操作關聯的符號的邏輯必須在BasicOperationExtendedOperation中重復。在這種情況下,這並不重要,因為很少的代碼是冗余的。如果有更多的共享功能,可以將其封裝在輔助類或靜態輔助方法中,以消除代碼冗余。

該條目中描述的模式在Java類庫中有所使用。例如,java.nio.file.LinkOption枚舉類型實現了CopyOptionOpenOption接口。

總之,雖然不能編寫可擴展的枚舉類型,但是你可以編寫一個接口來配合實現接口的基本的枚舉類型,來對它進行模擬。這允許客戶端編寫自己的枚舉(或其它類型)來實現接口。如果API是根據接口編寫的,那麽在任何使用基本枚舉類型實例的地方,都可以使用這些枚舉類型實例。

Effective Java 第三版——38. 使用接口模擬可擴展的枚舉