枚举类型是不可进行扩展的,但是有时候我们需要扩展该怎么办呢?目前还没有很好的方法来枚举基本类型的所有元素及其扩展。对于可伸缩的枚举类型而言,至少有一种具有说服力的用例,这就是操作码(operation code),也称opcode
。操作码是指这样的枚举类型:它的元素表示在某种机器上的那些操作,例如第43条中的Operation
类型,它表示一个简单的计算器中的某些函数,有时要尽可能地让API的用户提供它们自己的操作,这样可以有效的扩展API所提供的操作集。
幸运的是,有一种很好的方法可以利用枚举类型来实现这种效果。由于枚举类型可以通过操作码类型和(属于接口的标准实现)枚举定义接口来实现任意的接口,基本思想就是利用这一事实。例如,以下是Operation
类型的扩展版本:
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
接口:
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
方法。因为枚举为抽象的方法(apply)是接口(Operation )的一部分。
不仅可以在任何需要“基本枚举”的地方单独传递一个“扩展枚举”的实例,而且除了那些基本类型的元素之外,还可以传递完整的扩展枚举类型,并使用它的元素。例如,通过第34条的测试程序版本,体验一下上面定义过的所有扩展过的操作:
public static void main(String[] args) {
if (args.length < 2)
throw new AssertionError("参数错误");
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation1> void
test(Class<T> opEnumType, double x, double y){
for (Operation1 op: opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
注意扩展过的操作类型的类的字面文字(ExtendedOperation.class)从main
被传递给了test
方法,来描述被扩展操作的集合。这个类的字面文字充当其有限制的类型令牌。opEnumType
参数中公认很复杂的声明(<T extends Enum & Operation> Class)确保了Class
对象即表示枚举又表示Operation
的子类型,这正是遍历元素和执行每个相关联的操作时所需要的。
第二种方法是传入一个Collection<? Extends Operation>
,这是个有限制的通配符类型,而不传递一个类对象:
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 Operation1> opSet**, double x, double y){
for (Operation1 op: opSet)
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
这样得到的代码没有那么复杂,test
方法也更加灵活一些:它允许调用者将多个实现类型的操作合并到一起。另一方面,也放弃了在指定操作上使用EnumSet
和EnumMap
的功能。
上面这两段程序运行时带上命令行参数2和4, 都会产生如下输出:
2.000000 ^ 4.000000 = 16.000000
2.000000 % 4.000000 = 2.000000
用接口模拟可伸缩枚举有个小小的不足,即无法将实现从一个枚举类型继承到另一个枚举类型。如果实例代码不可依赖于任何状态,就可以将缺省实现放在接口中。在上述Operation
的示例中,保存和获取某项操作相关联的符号的逻辑代码,必须复制到ExtendedOperation
和BasicOperation
中。在这个例子中是可以的,因为复制的代码非常少。如果共享功能比较多,则可以将他们封装到一个辅助类或者静态辅助方法中,来避免代码的复制工作。
这种模式在Java类库中也得到了应用。例如,java.nio.file.LinkOption
枚举类型,它同时实现类CopyOption
和OpenOption
接口。
总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型来对它进行模拟。这样允许客户端编写自己的枚举类来实现接口。如果API是根据接口编写的,那么在可以使用基础枚举的任何地方,也都可以使用这些枚举。