[Effective Java]第六章 枚举和注解

第六章 枚举和注解

30、 用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型,例如一年中的季节或一副牌中的花色。在没引入枚举时,一般是声明一组int常量,每个类型成员一个常量:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

这种方法称作int枚举模式,存在很多不足,不具有类型安全与使用方便性。如果你将apple传到一个想要接收orange的方法中,编译器也不会出现警告,而且还可以使用==来比较apple与orange。

注意每个apple常量都以APPLE_作为前缀,每个orange常量都以ORANGE_作为前缀,这是因为可以防止名称冲突。

采用int枚举模式的程序是十分脆弱,因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译,如果不重新编译,程序还是可以运行,但不是最新的值了。

另外从使用方便性来看,没有便利的toString方法,打印出来的为数字,没有多大的用处。要遍历一组中所有的int枚举常量,也没有可靠的方法。

既然int枚举常量有这么多的缺点,那使用String枚举常如何?同样也不是我们期望的。虽然在可以打印字符串,但它会导致性能问题,因为它依赖于字符串的比较操作。另外与int枚举常量一样会编译到客户端代码中,编译时难以发现,但会在运行时出错。

幸运的是1.5版本开始,枚举可以避免int和String枚举模式的缺点,并提供许多额外的好处。下面是最简单的形式:

public enum Apple{FUJI,PIPPIN,GRANNY_SMITH}
public enum Orange{NAVEL,TEMPLE,BLOOD}

Java枚举类型背后的基本想法很简单:本质上是int值,它们是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。因为客户端即不能创建枚举类型的实例,也不能对它进行扩展,因此对它进行实例化,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例的泛型化,本质上是单元素的枚举。

枚举提供了编译时类型安全。如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象引用一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋值给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样,都会出错。

枚举提供了单独的命名空间,同一系统中可以有多个同名的枚举类型变量。你可以增加或者重新排序枚举类型常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式之中。最终,可以通过调用toString方法,将枚举转换成可打印的字符串。

除了完善了int枚举模式不足外,枚举还允许添加任意的方法和域,并实例任意接口,它们提供了所有Object(见第3章)的高级实现,实现了Comparable和Serializable接口,并针对枚举型的可任意改变性设计了序列化方式。

为什么要将方法或者域添加到枚举类型中?首先,你可能是想将数据与它的常量关联起来。例如,一个能够返回水果颜色或者返回水果图片的方法。第二,你可以利用任何适当的方法来增强枚举类型。第三,枚举类型可以先作为枚举常量的一个简单集合,随着实践的推移再演变成为全功能的抽象。

为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都因该为final。它们可以是公有的,但最好将它们做成私有的,并提供公有的访问方法。

与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中。这种行为最好被实现成私有的或者包级私有的方法。

有时候你需要将本质上不同的行为与每个常量关联起来,有一种方法是通过启用switch枚举的值来实现,有一种更好的办法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法。这种方法被称作特定于常量的方法实现。

枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身。如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。

可以为枚举类型添加数据域与方法,下面是一个算术运行的枚举类:

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

       Operation(String symbol) {//构造函数,存储操作符供toString打印使用
              this.symbol = symbol;
       }

       @Override
       //重写Enum中的打印name的性为
       public String toString() {
              return symbol;
       }

       //抽像方法,不同的常量具有不同的功能,需在每个常量类的主体里重写它
       abstract double apply(double x, double y);

       /*
        *  初始化时,存储操作符与枚举常量的对应关系,用来实现 fromString 方法
        *  这样我们就可以通过 操作符来获取到对应的枚举常量,有点像valueOf方法,
        *  只不过它是通过枚举常量的名字name来获取常量的。这种通用的方法还可以
        *  应用到其他枚举类中
        */
       private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
       static { // 从name到枚举常量转换到从某个域到枚举常量的转换
              for (Operation op : values())
                     stringToEnum.put(op.toString(), op);
       }

       // 根据操作符来获取对应的枚举常量,如果没有返回null,模拟valueOf方法
       public static Operation fromString(String symbol) {
              return stringToEnum.get(symbol);
       }

       public static void main(String[] args) {
              double x = Double.parseDouble(args[0]);
              double y = Double.parseDouble(args[1]);
              for (Operation op : Operation.values())
                     System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));

              for (Operation op : Operation.values())
                     System.out.printf("%f %s %f = %f%n", x, op, y, Operation
                                   .fromString(op.toString()).apply(x, y));
       }
}

在opr包下会看见Operation.class、Operation$4.class、Operation$2.class、Operation$3.class 、Operation$1.class这样几个类,Operation$X.class都是继承自Operation类,而Operation又继承自Enum类,下面是反编译这些类的代码:

public abstract class opr.Operation extends java.lang.Enum{
    public static final opr.Operation PLUS;
    public static final opr.Operation MINUS;
    public static final opr.Operation TIMES;
    public static final opr.Operation DIVIDE;
    private final java.lang.String symbol;
    private static final java.util.Map stringToEnum;
    private static final opr.Operation[] ENUM$VALUES;
    static {};
    private opr.Operation(java.lang.String, int, java.lang.String);
    public java.lang.String toString();
    abstract double apply(double, double);
    public static opr.Operation fromString(java.lang.String);
    public static void main(java.lang.String[]);
    public static opr.Operation[] values();
    public static opr.Operation valueOf(java.lang.String);
    opr.Operation(java.lang.String, int, java.lang.String, opr.Operation);
}
class opr.Operation$1 extends opr.Operation{
    opr.Operation$1(java.lang.String, int, java.lang.String);
    double apply(double, double);
}

枚举构造器不可以访问枚举的静态域,除了编译时常量域之外,这一限制是有必要的,因为构造器运行的时候,这些静态域还没有被初始化。

枚举常量中的方法有一个美中不足的地方,它们使用在枚举常量中共享代码变得更加因难了。例如,考虑用一个枚举来实现星期中的工资数。算法是这样的,在五个工作日中,除正常的工作时间外,算加班;在双休日中,所有工作时数都算加班时间,下面是第一次简单的实现:

public enum PayrollDay {
       MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
       private static final int HOURS_PER_SHIFT = 8;//正常工作时数

       /**
        * 工资计算
        * @param hoursWorked 工作时间(小时)
        * @param payRate 每小时工资
        * @return
        */
       double pay(double hoursWorked, double payRate) {
              //基本工资,注这里使用的是double,真实应用中请不要使用
              double basePay = hoursWorked * payRate;

              double overtimePay;//加班工资,为正常工资的1.5倍
              switch (this) {
              case SATURDAY:
              case SUNDAY://双休日加班工资
                     overtimePay = hoursWorked * payRate / 2;
              default: //正常工作日加班工资
                     overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0
                                   : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                     break;
              }
              return basePay + overtimePay;//基本工资+加班工资
       }
}

不可否认,这段代码很简单,但是从维护来看,非常危险。假设将一个元素添加到枚举中,如一个假期的特殊值,但忘了给switch语句添加相应的case,这时会计算出错。
为了针对不同的常量有不同的安全计算工资法,你必须重复每个常量的加班工资,或者将计算移到两个辅助方法中(一个用来计算工作日,一个用来计算双休日),并从每个常量调用相应的辅助方法。这任何一种方法都会产生很多的重复的样板代码,第二次如下实现:

public enum PayrollDay {
       MONDAY() {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekdayPay(hoursWorked, payRate);
              }
       },
       TUESDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekdayPay(hoursWorked, payRate);
              }
       },
       WEDNESDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekdayPay(hoursWorked, payRate);
              }
       },
       THURSDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekdayPay(hoursWorked, payRate);
              }
       },
       FRIDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekdayPay(hoursWorked, payRate);
              }
       },
       SATURDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekendPay(hoursWorked, payRate);
              }
       },
       SUNDAY {
              @Override
              double overtimePay(double hoursWorked, double payRate) {
                     return weekendPay(hoursWorked, payRate);
              }
       };
       private static final int HOURS_PER_SHIFT = 8;//正常工作时数

       //抽象出加班工资计算
       abstract double overtimePay(double hoursWorked, double payRate);

       //计算工资
       double pay(double hoursWorked, double payRate) {
              double basePay = hoursWorked * payRate;//公用
              return basePay + overtimePay(hoursWorked, payRate);
       }

       //双休日加班工资算法
       double weekendPay(double hoursWorked, double payRate) {
              return hoursWorked * payRate / 2;
       }

       //正常工作日加班工资
       double weekdayPay(double hoursWorked, double payRate) {
              return hoursWorked <= HOURS_PER_SHIFT ? 0
                            : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
       }
}

上面设计中存在很多的样板代码,如正常工作日都是调用weekdayPay方法来完成的,而双休都是调用weekendPay来完成的,有没有一种可以减少这些重复样板代码呢?请看下面:

enum PayrollDay {
       MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
                     PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
                     PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
       private final PayType payType;//策略枚举类

       PayrollDay(PayType payType) {
              this.payType = payType;
       }

       double pay(double hoursWorked, double payRate) {
              //计算委托给策略类
              return payType.pay(hoursWorked, payRate);
       }

       // 嵌套的枚举策略类
       private enum PayType {
              WEEKDAY {//工作日枚举策略实例常量
                     double overtimePay(double hours, double payRate) {
                            return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
                                          * payRate / 2;
                     }
              },
              WEEKEND {//双休日枚举策略实例常量
                     double overtimePay(double hours, double payRate) {
                            return hours * payRate / 2;
                     }
              };
              private static final int HOURS_PER_SHIFT = 8;

              abstract double overtimePay(double hrs, double payRate);

              double pay(double hoursWorked, double payRate) {
                     double basePay = hoursWorked * payRate;
                     return basePay + overtimePay(hoursWorked, payRate);
              }
       }
}

虽然这种模式没有前面两种那么简单,便更加安全,也更加灵活。

从上面加班工资计算三种实现来看,如果多个枚举常量同时共享相同的行为时,则考虑策略枚举。

如果枚举中的switch语句不是在枚举中实现特定于常量行为的一种很好的选择,那么它们还有什么用处呢?枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。例如,假设Operation枚举不受你的控制,你希望它有一个实例方法来返回每个运算的反运算。你可以用下列静态方法模拟这种效果:

public static Operation inverse(Operation op)
{
    switch(op)
    {
        case PLUS:    return Operation.MINUS;
        case MINUS:   return Operation.PLUS;
        case TIMES:   return Operation.DIVIDE;
        case DIVIDE:  return Operation.TIMES;
        default:  throw new AssertionError("Unknow op:"+op);
    }
}

枚举优先使用comparable而非int常量。与int常量相比,枚举有个小小的性能缺点,即装载和初始化时会有空间和时间的成本。除了受资源约束的设备,例如手机和烤面包机之外,在实践中不必太在意这个问题。

那么什么时候应该使用枚举呢?每当需要一组固定常量的时候。当然,这包括“天然的枚举类型“,例如行星,一周的天数以及棋子的数目等等。但它也包括你在编译时就知道其所有可能值的其他集合,例如菜单的选项,操作代码以及命令行标记等。枚举类型中的常量集并不一定要始终保持不变。

枚举适用于一组固定常量,当然枚举类型中的常量集并不一定要始终保持不变。

总而言之,与int常量相比,枚举类型的优势是不言而喻的。枚举要易读地多,也更加安全,功能也更加强大。许多枚举都不需要显式地构造器或成员,但许多其他枚举则受益于“每个常量与属性的关联“以及“提供行为受这个属性影响的方法“。只有极少数的枚举受益于多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑策略枚举。

31、 不要使用ordinal,用实例域代替序数

永远不要根据枚举序数ordinal()导出与它关联的值,即不要依赖于枚举序数,否则重新排序这些枚举或添加新的常量,维护起来将是很困难的:

public enum Ensemble {
       SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET;
       public int numberOfMusicians() {
              return ordinal() + 1;
       }
}
我们要将它保存在一个实例域中:
public enum Ensemble {
       SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7);
       private final int numberOfMusicians;

       Ensemble(int size) {
              this.numberOfMusicians = size;
       }

       public int numberOfMusicians() {
              return numberOfMusicians;
       }
}

Enum规范中谈到ordinal时这么定道:“大多数程序员都不需要这个方法。它是设计成用于像EunmSet和EnumMap这种基于枚举的通用数据结构”,除非你在编写的是这种数据结构,否则最好完全避免使用ordinal方法。

32、 用EnumSet代替位域

如果一个枚举类型的元素主要用在集合(组合)中,一般就使用int枚举模式,做法是将1向左移位来实现,这样就会有很多的组合形式,下面是四种字体样式的应用,可以组合出 2^4 – 1 = 15种样式来:

class Text{
       public static final int STYLE_BOLD = 1 << 0;//1 字体加粗
       public static final int STYLE_ITALTC = 1 << 1;// 2 斜体
       public static final int STYLE_UNDERLINE = 1 << 2;//4 下划线
       public static final int STYLE_STRIKETHROUGH = 1 << 3;//8 删除线

       //应用样式
       public void applyStyles(int styles){
              //...
       }
       public static void main(String[] args) {
              //应用粗体与斜体组合样式
              new Text().applyStyles(STYLE_BOLD|STYLE_ITALTC);
       }
}

位域表示法允许利用位操作,有效地执行了像组合和交集这样的集合操作。但位域有着int枚举常量的所有缺点,甚至更多,如当位域以数字形式打印时,翻译位域比翻译简单的(单个的)枚举常要困难得多。那么有没有一种好的方案来代替上面的设计呢?使用EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。EnumSet内容都表示为位矢量,如果底层的枚举类型有64个或者更少的元素——大多如此——整个EnumSet就是用单个long来表示,因此它的性能比得上位域的性能。批处理,如removeAll和retainAll,都是利用位算法来实现的,就像手工替位域实现那样,但可以避免手工位操作时容易出现的错误以及复杂的代码。

下面是前一个实例改用枚举代替位域后的代码,它更加简短、清楚、安全:

public class Text {
       public enum Style {
              BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
       }

       /*
        *  这里使用的Set接口而不是EnumSet类,最好还是使用接口
        *  类型而非实现类型,这样还可以传递一些其他的Set实现
        */
       public void applyStyles(Set<Style> styles) {

       }

       // Sample use
       public static void main(String[] args) {
              Text text = new Text();
              text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
       }
}

总之,正是因为枚举类型可用在集合EnumSet中,所以没有理由用位域来表示它。EnumSet类集位域的简洁和性能优势及第30条中所述的枚举类型的所有优点于一身,用EnumSet代替位域就是理所当然的了。

33、 用EnumMap代替序数索引

EnumMap:与枚举类型键一起使用的专用 Map 实现。枚举映射中所有键都必须来自单个枚举类型,该枚举类型在创建映射时显式或隐式地指定。枚举映射在内部表示为数组。此表示形式非常紧凑且高效。

先来看一个能植物分类的实例,分类的标准是按照某枚举类型来分的:

public class Herb {
       // 植物各类:一年生、多年生、两年生
       static public enum Type {
              ANNUAL, PERENNIAL, BIENNIAL
       }

       private final String name;//植物名字
       private final Type type;//植物各类

       Herb(String name, Type type) {
              this.name = name;
              this.type = type;
       }

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

       public static void main(String[] args) {
              //现有这样一些植物集合
              Herb[] garden = { new Herb("Basil", Type.ANNUAL),
                            new Herb("Carroway", Type.BIENNIAL),
                            new Herb("Dill", Type.ANNUAL),
                            new Herb("Lavendar", Type.PERENNIAL),
                            new Herb("Parsley", Type.BIENNIAL),
                            new Herb("Rosemary", Type.PERENNIAL) };
              //数组的索引与枚举Type对应 //问题一:需要进行未受检的转换
              Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
              //初始化
              for (int i = 0; i < herbsByType.length; i++) {
                     herbsByType[i] = new HashSet<Herb>();
              }
              //开始分类操作
              for (Herb h : garden) {
                     //根据序数取出对应的容器再放入
                     herbsByType[h.type.ordinal()].add(h);
              }

              //输出
              for (int i = 0; i < herbsByType.length; i++) {
                     System.out//问题二:手工输出类别,还有可能引发数组越界
                                   .printf("%s: %s%n", Herb.Type.values()[i], herbsByType[i]);
              }
       }
}

输出:

ANNUAL: [Basil, Dill]
PERENNIAL: [Rosemary, Lavendar]
BIENNIAL: [Carroway, Parsley]

使用EnumMap对上面进行改进:

Herb[] garden =...
// 使用EnumMap并按照植物种类(枚举类型)来分类
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
              Herb.Type.class);
for (Herb.Type t : Herb.Type.values())//初始化
       herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)//进行分类
       herbsByType.get(h.type).add(h);
System.out.println(herbsByType);//输出

EnumMap在内部使用数组实现,在性能上与数组相当。但是它对程序员隐藏了实现细节,集Map的丰富功能和类型安全与数组的快速于一身。

如果这种关系是多维的,就使用EnumMap<…, EnumMap<…>>

// Using a nested EnumMap to associate data with enum pairs - Pags 163-164

import java.util.*;

public enum Phase {
   SOLID, LIQUID, GAS;

   public enum Transition {
      MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
      BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),
      SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

      private final Phase src;
      private final Phase dst;

      Transition(Phase src, Phase dst) {
         this.src = src;
         this.dst = dst;
      }
      // Initialize the phase transition map
      private static final Map<Phase, Map<Phase,Transition>> m =
        new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
      static {
         for (Phase p : Phase.values())
           m.put(p,new EnumMap<Phase,Transition>(Phase.class));
         for (Transition trans : Transition.values())
           m.get(trans.src).put(trans.dst, trans);
      }

      public static Transition from(Phase src, Phase dst) {
         return m.get(src).get(dst);
      }
   }

   // Simple demo program - prints a sloppy table
   public static void main(String[] args) {
       for (Phase src : Phase.values())
           for (Phase dst : Phase.values())
               if (src != dst)
                   System.out.printf("%s to %s : %s %n", src, dst,
                                     Transition.from(src, dst));
   }
}

总而言之,最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap<…,EnumMap<…>>。应用程序的程序员在一般情况下都不使用Enum.ordinal,即使要用也很少,因此这是一种特殊情况。

34、 用接口模拟可伸缩的枚举

枚举类型是不能被扩展的(继承),但使用接口可以解决这一问题,解决办法是让枚举类实现同一接口,在应用的地方以接口类型来传递参数,但这样会失去Enum类的某些特性:

// 枚举接口
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;
       }
}
//扩展运算
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;
       }
}
测试:
private static <T extends Enum<T> & Operation> void test(Class<T> opSet,
              double x, double y) {
       for (Operation op : opSet.getEnumConstants())//失去Enum特性,使用反射
              System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
public static void main(String[] args) {
       double x = Double.parseDouble(args[0]);
       double y = Double.parseDouble(args[1]);
       test(BasicOperation.class, x, y);
       test(ExtendedOperation.class, x, y);
}
输出:
4.000000 + 2.000000 = 6.000000
4.000000 - 2.000000 = 2.000000
4.000000 * 2.000000 = 8.000000
4.000000 / 2.000000 = 2.000000
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000

用接口模拟可伸缩枚举有个小小的不足,即无法将实现从一个枚举类型继承到另一个枚举类型。在上述Operation的实例中,保存和获取与某项操作相关联的符号的逻辑代码,可以复制到BasicOperation和ExtendedOperation中。在这个例子中是可以的,因为复制的代码比较少。如果共享的代码比较多,则可以将它封装在一个辅助类或者静态辅助方法中,来避免代码的复制工作。

总之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样,客户端就能够编写自己的枚举来实现接口,如果API是根据接口编写的,那么在可以使用基础枚举类型的地方,也都可以使用这些枚举。

35、 注解优先于命名模式

命名模式,表示有些程序元素需要通过某种工具或者框架进行特殊处理。例如,Junit框架原本要求它的用户一定要使用test作为测试方法名称的开头。

下面是一个简单的测试框架,使用注解来实现:

//专用于普通测试注解,该注解只适用于静态的无参方法,
//如果使用地方不正确由注解工具自己把握
@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行时,这样工具可以使用
@Target(ElementType.METHOD)//只适用于方法
public @interface Test {}

//专用于异常测试的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
       //测试方法可能抛出多个异常
       Class<? extends Exception>[] value();
}

下面应用上面定义的注解:

public class Sample {
       @Test
       public static void m1() {} // 测试应该通过
       public static void m2() {}
       @Test
       public static void m3() { // 测试应该失败
              throw new RuntimeException("Boom");
       }
       public static void m4() {}
       @Test//不应该使用在这里,但应该由注解工具自己处理这种不当的使用
       public void m5() {} // 错误使用: 非静态方法
       public static void m6() {}
       @Test
       public static void m7() { // 测试应该失败
              throw new RuntimeException("Crash");
       }
       public static void m8() {}
}
public class Sample2 {
       @ExceptionTest(ArithmeticException.class)
       public static void m1() { // 测试应该要通过,因为抛出了算术异常
              int i = 0;
              i = i / i;
       }
       @ExceptionTest(ArithmeticException.class)
       public static void m2() { // 测试应该不通过,因为抛出的异常为数组越界异常
              int[] a = new int[0];
              int i = a[1];
              System.out.println(i);
       }
       @ExceptionTest(ArithmeticException.class)
       public static void m3() {
       } // 测试应该不通过,因为没有抛也异常

       // 可能抛出多个异常,使用{}括起来,如果是单个可以省略
       @ExceptionTest( { IndexOutOfBoundsException.class,
                     NullPointerException.class })
       public static void doublyBad() {
              List<String> list = new ArrayList<String>();
              //这里会抛出空指针错误,测试应该会通过
              list.addAll(5, null);
       }
}

T注解对应用类的语义没有直接的影响。注解永远不会改变被注解代码的语义,但是使用它可以通过工具进行特殊的处理,如下面注解工具实现类:

public class RunTests {
       public static void main(String[] args) throws Exception {
              int tests = 0;//需要测试的数量
              int passed = 0;//测试通过的数量
              //加载被注解的类,即被测试的类
              Class<?> testClass = Class.forName(args[0]);
              //遍历测试类的所有方法
              for (Method m : testClass.getDeclaredMethods()) {

                     //Test注解实现工具
                     if (m.isAnnotationPresent(Test.class)) {
                            tests++;
                            try {
                                   m.invoke(null);//没有参数,表示调用的是静态方法
                                   passed++;//如果方法调用成功则表示测试通过
                            }//表示测试方法本身抛出了异常
                            catch (InvocationTargetException wrappedExc) {
                                   Throwable exc = wrappedExc.getCause();
                                   //打印测试方法抛出的异常信息
                                   System.out.println(m + " failed: " + exc);
                            }//如果抛异常表示注解使用错误,不应使用在非静态或带参数的方式上 
                            catch (Exception exc) {
                                   //打印测试未通过的方法信息
                                   System.out.println("使用@Test注解错误的方法 : " + m);
                            }
                     }

                     // ExceptionTest注解实现工具
                     if (m.isAnnotationPresent(ExceptionTest.class)) {
                            tests++;
                            try {
                                   m.invoke(null);
                                   //如果注解工具运行到这里,则测试方法未抛出异常,但属于测试未通过
                                   System.out.printf("Test %s failed: no exception%n", m);
                            } catch (Throwable wrappedExc) {
                                   //获取异常根源
                                   Throwable exc = wrappedExc.getCause();
                                   //取出注解的值
                                   Class<? extends Exception>[] excTypes = m.getAnnotation(
                                                 ExceptionTest.class).value();
                                   int oldPassed = passed;
                                   //将根源异常与注解值对比
                                   for (Class<? extends Exception> excType : excTypes) {
                                          //如果测试方法抛出的异常与注解中预期的异常匹配则表示测试通过
                                          if (excType.isInstance(exc)) {//使用动态的instance of
                                                 passed++;
                                                 break;
                                          }
                                   }
                                   //打印测试没有通过的方法信息
                                   if (passed == oldPassed)
                                          System.out.printf("Test %s failed: %s %n", m, exc);
                            }
                     }
              }
              //打印最终测试结果,通过了多少个,失败了多少个
              System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
       }
}

下面是两次运行输出结果(前面的为测试Sample类,后面是测试Sample1类):

public static void Item35.Sample.m3() failed: java.lang.RuntimeException: Boom
使用@Test注解错误的方法 : public void Item35.Sample.m5()
public static void Item35.Sample.m7() failed: java.lang.RuntimeException: Crash
Passed: 1, Failed: 3

Test public static void Item35.Sample2.m2() failed: java.lang.ArrayIndexOutOfBoundsException: 1
Test public static void Item35.Sample2.m3() failed: no exception
Passed: 2, Failed: 2

本条目中开发的测试框架只是一个试验,但它清楚地示范了注解优于命名模式,这只是揭开注解功能的冰山一角。如果在编写一个需要程序员给源文件添加信息的工具,就要定义一组适当的注解类型。另外,我们要考虑使用Java平台提供的预定义的标准注解类型(见第36条)。还要考虑使用IDE或者静态分析工具所提供的任何注解。这种注解可以提升由这些工具所提供的诊断信息的质量。但是还要注意这些注解还没有标准化,因此如果变换工具或者形成标准,就有很多工作要做了。

36、 坚持使用Override注解

当你打算重写一个方法时,有可能写成重载,这时如果@Override注解就可以防止出现这个问题。
这个经典示例涉及equals方法,程序员可以编写如下代码:

public boolean equals (Foo that){…}

当你应当把他们编写成如下时:

public Boolean equals(Object that)

这也是合法的,但是类Foo从Object继承了equals实现,最终成了重载,而原本是重写的,这时我们可以使用@Override在重写的方法前,这样如果在没有重写的情况下,编译器则会提示我们。

IDE具有自动检查代码的功能,称作代码检验。如果启动相应的代码检验功能,当有一个方法没有Override注解,却覆盖了超类的方法时,IDE就会产生一条警告。

注,@Override不能用在实现父接口中的方法前面,因为这叫实现不叫重写了,这与可以加在实现父抽象类中方法前是不一样的。虽然这么写是可以的,但绝对没有必要,因为你的类没有实现每一个接口方法,编译器就会产生一条错误消息。

总之,如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器可以替你防止大量的错误,但有一个例外,在具体的类中,不必标注你确认覆盖了抽象方法声明的方法,虽然这么做也没有什么坏处。

37、 用标记接口定义类型

标记接口是没有包含方法声明的接口,而只是指明(或者“标明”)一个类实现了具有某种属性的接口。如,Serializable接口,通过实现这个接口,表明它的实例可以被写到ObjectOutputStream中(或者“被序列化”)。

标记注解,没有参数,只是“标注”被注解的元素,如果第 35 条的@Test就是一个标记注解。

标记注解并不能替代标记接口。标记接口有两点胜过标记注解。最重要的一点是,标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。标记接口在编译时就会被检测是否有错误,而标记注解则要到运行时期(但也并非如此,就Serializable标记接口而言,如果它的参数没有实现该接口,ObjectOutputStream.write(Object)方法将会失败,但令人不解的是,ObjectOutputStream API的创建者在声明Write方法时并没有利用Serializable接口,该方法的参数类型应该为Serializable而非Object,因此,试着在没有实现Serializable的对象上调用ObjectOutputStream.write,只会在运行时出错,所以也并不是像前面说的那样)。

HYPERLINK “mailto:如果你正在编写的是目标为@Target(ElementType.TYPE)” 如果你正在编写的是目标为@Target(ElementType.TYPE)的标记注解类型,就要考虑使用标记接口来实现呢。因为标记接口可以更加精确地进行锁定。如果注解类型利用@Target(ElementType.TYPE)声明,它就可以被应用到任何类或者接口。假如有一个标记只试用于特殊接口的实现。如果它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口。

标记注解胜过于标记接口的最大优点在于,它可以通过默认的方式增加一个或者多个标记注解类型元素,给已被使用的注解类型添加更多的信息。随着时间的推移,简单的标记注解类型可以演变成更加丰富的注解类型。这种演变对于标记接口而言则是不可能的,因为它不可能在实现接口之后再给它添加方法。

标记注解的另一各优点在于,它们是更大的注解机制的一部分,因此,标记注解再那些支持注解为变成元素之一的框架中具有一致性。

那么什么时候应该使用标记注解,什么时候应该使用标记接口呢?很显然,如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现后者扩展接口。如果标记只应用给类和接口,就要问问自己:我要编写一个还是多个只接受有这种标记的方法呢?如果是这种情况,就应该优先使用标记接口而非标记注解。这样你就可以用接口相关的参数类型,它真正可以为你提供编译时进行类型检查的好处。

如果你对第一个问题的回答是否定的,就要再问问自己:我要永远限制这个标记只适用于特殊接口的元素吗?如果是,最好将标记定义成该接口的一个子接口。如果这两个问题的答案都是否定的,或许就应该使用标记注解。

总之,接口是用来定义类型的,而注解是用来辅助分析类元素信息的。各有各的用处。如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择。如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择。如果你发现自己在编写的是目标为ElementType.TYPE的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适呢。

从某种意义上说,本条目与第19条中“如果不想定义类型就不要使用接口”的说法相反。本科目最接近的意思是说:“如果想要定义类型,一定要使用接口”。

转载自:http://www.cnblogs.com/jiangzhengjun/p/4255663.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值