美文网首页Effective Java
第34条:用接口模拟可伸缩的枚举

第34条:用接口模拟可伸缩的枚举

作者: 徐大力Bear | 来源:发表于2017-06-16 14:20 被阅读0次

    枚举类型是不可扩展的,但是接口类型是可扩展的。使用接口,可以模拟可伸缩的枚举。

    就几乎所有方面来看,枚举类型都优越于类型安全枚举模式。从表面上看,有一个异常与伸缩性有关,这个异常可能处在原来的模式中,却没有得到语言构造的支持。换句话说,使用这种模式,就有可能让一个枚举类型去扩展另一个枚举类型;利用这种语言特性,则不可能这么做。这绝非偶然。枚举的可伸缩性最后证明基本上都不是什么好点子。扩展类型的元素是基本类型的实例,基本类型的实例却不是扩展类型的元素,这样很混乱。

    类型安全枚举模式:定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数。相反,提供公有的静态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是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也就可以使用这些枚举。

相关文章

网友评论

    本文标题:第34条:用接口模拟可伸缩的枚举

    本文链接:https://www.haomeiwen.com/subject/hqkpqxtx.html