Effective Java 第三版——38. 使用接口模擬可擴展的枚舉
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));
}
生成的代碼稍微不那麽復雜,tes
t方法靈活一點:它允許調用者將多個實現類型的操作組合在一起。另一方面,也放棄了在指定操作上使用EnumSe
t(條目 36)和EnumMap
(條目 37)的能力。
上面的兩個程序在運行命令行輸入參數4和2時生成以下輸出:
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
使用接口來模擬可擴展枚舉的一個小缺點是,實現不能從一個枚舉類型繼承到另一個枚舉類型。如果實現代碼不依賴於任何狀態,則可以使用默認實現(條目 20)將其放置在接口中。在我們的Operation
示例中,存儲和檢索與操作關聯的符號的邏輯必須在BasicOperation
和ExtendedOperation
中重復。在這種情況下,這並不重要,因為很少的代碼是冗余的。如果有更多的共享功能,可以將其封裝在輔助類或靜態輔助方法中,以消除代碼冗余。
該條目中描述的模式在Java類庫中有所使用。例如,java.nio.file.LinkOption
枚舉類型實現了CopyOption
和OpenOption
接口。
總之,雖然不能編寫可擴展的枚舉類型,但是你可以編寫一個接口來配合實現接口的基本的枚舉類型,來對它進行模擬。這允許客戶端編寫自己的枚舉(或其它類型)來實現接口。如果API是根據接口編寫的,那麽在任何使用基本枚舉類型實例的地方,都可以使用這些枚舉類型實例。
Effective Java 第三版——38. 使用接口模擬可擴展的枚舉