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接口,并针对枚举型的可任意改变性设计了序列化方式。

 

如果一个枚举具有普遍适用性,它就应该成为一个顶层类,如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类。

 

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

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 stringToEnum = new HashMap();

       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 {

       MONDAYTUESDAYWEDNESDAYTHURSDAYFRIDAYSATURDAYSUNDAY;

       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);

              }

       }

}

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

 

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

 

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

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

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

public enum Ensemble {

       SOLODUETTRIOQUARTETQUINTETSEXTETSEPTET;

       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 {

              BOLDITALICUNDERLINESTRIKETHROUGH

       }

 

       /*

        *  这里使用的Set接口而不是EnumSet类,最好还是使用接口

        *  类型而非实现类型,这样还可以传递一些其他的Set实现

        */

       public void applyStyles(Set