枚举类型是不可扩展的,但是接口类型是可扩展的。使用接口,可以模拟可伸缩的枚举。
就几乎所有方面来看,枚举类型都优越于类型安全枚举模式。从表面上看,有一个异常与伸缩性有关,这个异常可能处在原来的模式中,却没有得到语言构造的支持。换句话说,使用这种模式,就有可能让一个枚举类型去扩展另一个枚举类型;利用这种语言特性,则不可能这么做。这绝非偶然。枚举的可伸缩性最后证明基本上都不是什么好点子。扩展类型的元素是基本类型的实例,基本类型的实例却不是扩展类型的元素,这样很混乱。
类型安全枚举模式:定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数。相反,提供公有的静态final域,使枚举类型中的每一个常量都对应一个域。
public class Suit {
private final String name;
private Suit(String name){
this.name = name;
}
public String toString(){
return name;
}
public static final Suit CLIBS = new Suit("clubs");
public static final Suit DIAMONDS = new Suit("diamonds");
public static final Suit HEARTS = new Suit("hearts");
public static final Suit SPADES = new Suit("spades");
}
要想类型安全模式可以被扩展,只要提供一个受保护的构造函数就可以。然后其他人就可以扩展这个类,并且在子类中增加新的常量。类型安全枚举模式可扩展变形与可序列化变形是兼容的,但是两者组合的时候要非常小心,每个子类必须分配自己的序数,并且提供自己的readResolve方法。下边的Operation类是可扩展的、可序列化的类型安全枚举类(每个,枚举类型极其定义的枚举变量在JVMh中都是唯一的):
public abstract class Operation1 implements Serializable{
private final transient String name;//标记为transient的变量不会被序列化
protected Operation1(String name) {
this.name = name;
}
public String toString(){
return this.name;
}
public static Operation1 PLUS = new Operation1("+"){
protected double eval(double x, double y) {
return x + y;
}
};
public static Operation1 MINUS = new Operation1("-"){
protected double eval(double x, double y) {
return x - y;
}
};
public static Operation1 TIMES = new Operation1("*"){
protected double eval(double x, double y) {
return x * y;
}
};
public static Operation1 DIVIDE = new Operation1("/"){
protected double eval(double x, double y) {
return x / y;
}
};
protected abstract double eval(double x, double y);
private static int nextOrdinal = 0;
private final int ordinal = nextOrdinal++;
private static final Operation1[] VALUES ={PLUS,MINUS,TIMES,DIVIDE};
Object readResolve() throws ObjectStreamException{
return VALUES[ordinal];
}
下面是Operation的一个子类,它增加了对数和指数的操作。他本身也是可以扩展的。
public abstract class ExtendedOperation1 extends Operation1 {
ExtendedOperation1(String name) {
super(name);
}
public static Operation1 LOG = new Operation1("log"){
protected double eval(double x, double y) {
return Math.log(y)/Math.log(x);
}
};
public static Operation1 EXP = new Operation1("exp"){
protected double eval(double x, double y) {
return x + y;
}
};
private static int nextOrdinal = 0;
private final int ordinal = nextOrdinal++;
private static final Operation1[] VALUES = {LOG,EXP};
Object readResolve() throws ObjectStreamException{
return VALUES[ordinal];
}
有一种很好的方法来实现可伸缩性的枚举,以下是第30条中的Operation类型的扩展版本:
/**
* 虽然枚举类型是不能扩展的 , 但是可以通过接口类表示API中的操作的接口类型 .
* 你可以定义一个功能完全不同的枚举类型来实现这个接口 .
*/
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;
}
}
你可以定义另外一个枚举类型,它实现这个接口,并用这个新类型的实例代替基本类型。例如,假设你想要定义一个上述操作类型的扩展,由求幂和求余操作组成。你所要做的就是编写一个枚举类型,让它实现Operation接口:
public enum ExtendedOperation implements Operation {
EXP("^"){
@Override
public double apply(double x, double y) {
return Math.pow(x , y);
}
},
REMAINDER("%"){
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
}
注意,在枚举中,不必像在不可扩展的枚举中所做的那样,利用特定于实例的方法实现来声明抽象的apply方法。这是因为抽象的方法是接口的一部分。
不仅可以在任何需要“基本枚举”的地方单独传递一个"扩展枚举"的实例,而且除了那些基本类型的元素之外,还可以传递完整的扩展枚举类型,并使用它的元素。通过下面这个测试程序,体验一下上面定义过的所有扩展过的操作:
public class test1 {
public static void main(String[] args) {
double x = 2;
double y = 4;
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opSet,
double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f= %f%n", x, op, y, op.apply(x, y));
}
}
第二种方法是使用Collection<? extends Operation>,这个有限的通配符类型,作为opSet参数的类型:
import java.util.Arrays;
import java.util.Collection;
public class test2 {
public static void main(String[] args) {
double x = 2;
double y = 4 ;
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));
}
}
}
上面这两段程序用命令行参数 2和4运行时,都会产生这样的输出:
2.000000 ^ 4.000000= 16.000000
2.000000 % 4.000000= 2.000000
用接口模拟可伸缩枚举有个小小的不足,即无法将实现从一个枚举类型继承到另一个枚举类型。在上Operation的实例中,保存和获取与某项操作相关联的符号的逻辑代码,可以复制到BasicOperation和ExtendedOperation中。在这个例子中是可以的,因为复制的代码非常少。如果共享功能比较多,则可以将它封装在一个辅助类或者静态辅助方法中,来避免代码的复制工作。
总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样允许客户端编写自己的枚举来实现接口。如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也就可以使用这些枚举。