Java枚举类型的基本想法:通过公有的静态final域为每个枚举常量导出一个实例,枚举类型没有构造器,是个真正的final类。枚举类型是单例的。

枚举类型的优势

可以添加任意的方法和域,并实现任意 接口。他们提供了Object方法的高级实现,并且实现了Comparable和Serializable接口。下面编写一个Planet枚举类型,注意其中构造函数的写法,这个构造函数并不会被客户端使用,而是Planet自己内部使用,传入参数的形式是通过枚举名传入参数值的,因为枚举类型是不可变的,因此枚举类型内部的所有变量都是final的,最好都写成私有,提供公共的访问方法。

  1. public enum Planet {
  2. MERCURY(3.302e+23, 2.439e6),
  3. VENUS (4.869e+24, 6.052e6),
  4. EARTH (5.975e+24, 6.378e6),
  5. MARS (6.419e+23, 3.393e6),
  6. JUPITER(1.899e+27, 7.149e7),
  7. SATURN (5.685e+26, 6.027e7),
  8. URANUS (8.683e+25, 2.556e7),
  9. NEPTUNE(1.024e+26, 2.477e7);
  10. private final double mass; // In kilograms
  11. private final double radius; // In meters
  12. private final double surfaceGravity; // In m / s^2
  13. // Universal gravitational constant in m^3 / kg s^2
  14. private static final double G = 6.67300E-11;
  15. // Constructor
  16. Planet(double mass, double radius) {
  17. this.mass = mass;
  18. this.radius = radius;
  19. surfaceGravity = G * mass / (radius * radius);
  20. }
  21. public double mass() { return mass; }
  22. public double radius() { return radius; }
  23. public double surfaceGravity() { return surfaceGravity; }
  24. public double surfaceWeight(double mass) {
  25. return mass * surfaceGravity; // F = ma
  26. }
  27. }

下面写出一个计算行星质量的代码:

  1. public class WeightTable {
  2. public static void main(String[] args) {
  3. double earthWeight = Double.parseDouble(args[0]);
  4. double mass = earthWeight / Planet.EARTH.surfaceGravity();
  5. for (Planet p : Planet.values())
  6. System.out.printf("Weight on %s is %f%n",
  7. p, p.surfaceWeight(mass));
  8. }
  9. }

如果将已有枚举类型中的某个类型移除会使得之前没有引用的该类型的继续工作,之前引入的如果没有重新编译客户端也可以继续工作,如果重新编译了会提出一条错误信息,这已经是可以做到的最好的行为了。
如果一个枚举类具有普遍适用性,那就应该成为一个顶层类,如果只是被用于一个特定的顶层类中,它就应该成为该顶层类的成员类。

2.给每个类型绑定不同的方法

希望对不同的枚举类型常量绑定不同的方法,可以使用if…else…条件语句判断,如下,但这样并不好如果再新加入一个常量,却忘记给switch加入相应的条件,枚举依然可以编译,但是当你尝试使用新的运算时,就会失败。

  1. // Enum type that switches on its own value - questionable
  2. public enum Operation {
  3. PLUS, MINUS, TIMES, DIVIDE;
  4. // Do the arithmetic operation represented by this constant
  5. public double apply(double x, double y) {
  6. switch(this) {
  7. case PLUS: return x + y;
  8. case MINUS: return x - y;
  9. case TIMES: return x * y;
  10. case DIVIDE: return x / y;
  11. }
  12. throw new AssertionError("Unknown op: " + this);
  13. }
  14. }

幸运的是,有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的方法实现:

  1. public enum Operation {
  2. PLUS {public double apply(double x, double y){return x + y;}},
  3. MINUS {public double apply(double x, double y){return x - y;}},
  4. TIMES {public double apply(double x, double y){return x * y;}},
  5. DIVIDE{public double apply(double x, double y){return x / y;}};
  6. public abstract double apply(double x, double y);
  7. }

特定于常量的方法实现可以与特定于常量的数据结合使用。 例如,以下是Operation的一个版本,它重写toString方法以返回通常与该操作关联的符号:

  1. // Enum type with constant-specific class bodies and data
  2. public enum Operation {
  3. PLUS("+") {
  4. public double apply(double x, double y) { return x + y; }
  5. },
  6. MINUS("-") {
  7. public double apply(double x, double y) { return x - y; }
  8. },
  9. TIMES("*") {
  10. public double apply(double x, double y) { return x * y; }
  11. },
  12. DIVIDE("/") {
  13. public double apply(double x, double y) { return x / y; }
  14. };
  15. private final String symbol;
  16. Operation(String symbol) { this.symbol = symbol; }
  17. @Override public String toString() { return symbol; }
  18. public abstract double apply(double x, double y);
  19. }

显示的toString实现可以很容易地打印算术表达式,正如这个小程序所展示的那样:

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

枚举类型有个自动产生的valueOf(String)方法,他将常量的名字转变成常量本身

特定于常量的方法实现的不足与改进

例如,考虑一个代表工资包中的工作天数的枚举。 该枚举有一个方法,根据工人的基本工资(每小时)和当天工作的分钟数计算当天工人的工资。 在五个工作日内,任何超过正常工作时间的工作都会产生加班费; 在两个周末的日子里,所有工作都会产生加班费。 使用switch语句,通过将多个case标签应用于两个代码片段中的每一个,可以轻松完成此计算:

  1. // Enum that switches on its value to share code - questionable
  2. enum PayrollDay {
  3. MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
  4. SATURDAY, SUNDAY;
  5. private static final int MINS_PER_SHIFT = 8 * 60;
  6. int pay(int minutesWorked, int payRate) {
  7. int basePay = minutesWorked * payRate;
  8. int overtimePay;
  9. switch(this) {
  10. case SATURDAY: case SUNDAY: // Weekend
  11. overtimePay = basePay / 2;
  12. break;
  13. default: // Weekday
  14. overtimePay = minutesWorked <= MINS_PER_SHIFT ?
  15. 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
  16. }
  17. return basePay + overtimePay;
  18. }
  19. }

这段代码无可否认是简洁的,但从维护的角度来看是危险的。 假设你给枚举添加了一个元素,可能是一个特殊的值来表示一个假期,但忘记在switch语句中添加一个相应的case条件。 该程序仍然会编译,但付费方法会默默地为工作日支付相同数量的休假日,与普通工作日相同。
要使用特定于常量的方法实现安全地执行工资计算,必须为每个常量重复加班工资计算,或将计算移至两个辅助方法,一个用于工作日,另一个用于周末,并调用适当的辅助方法来自每个常量。 这两种方法都会产生相当数量的样板代码,大大降低了可读性并增加了出错机会。
通过使用执行加班计算的具体方法替换PayrollDay上的抽象overtimePay方法,可以减少样板。 那么只有周末的日子必须重写该方法。 但是,这与switch语句具有相同的缺点:如果在不重写overtimePay方法的情况下添加另一天,则会默默继承周日计算方式。
你真正想要的是每次添加枚举常量时被迫选择加班费策略。 幸运的是,有一个很好的方法来实现这一点。 这个想法是将加班费计算移入私有嵌套枚举中,并将此策略枚举的实例传递给PayrollDay枚举的构造方法。 然后,PayrollDay枚举将加班工资计算委托给策略枚举,从而无需在PayrollDay中实现switch语句或特定于常量的方法实现。 虽然这种模式不如switch语句简洁,但它更安全,更灵活:

  1. // The strategy enum pattern
  2. enum PayrollDay {
  3. MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
  4. SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
  5. private final PayType payType;
  6. PayrollDay(PayType payType) { this.payType = payType; }
  7. PayrollDay() { this(PayType.WEEKDAY); } // Default
  8. int pay(int minutesWorked, int payRate) {
  9. return payType.pay(minutesWorked, payRate);
  10. }
  11. // The strategy enum type
  12. private enum PayType {
  13. WEEKDAY {
  14. int overtimePay(int minsWorked, int payRate) {
  15. return minsWorked <= MINS_PER_SHIFT ? 0 :
  16. (minsWorked - MINS_PER_SHIFT) * payRate / 2;
  17. }
  18. },
  19. WEEKEND {
  20. int overtimePay(int minsWorked, int payRate) {
  21. return minsWorked * payRate / 2;
  22. }
  23. };
  24. abstract int overtimePay(int mins, int payRate);
  25. private static final int MINS_PER_SHIFT = 8 * 60;
  26. int pay(int minsWorked, int payRate) {
  27. int basePay = minsWorked * payRate;
  28. return basePay + overtimePay(minsWorked, payRate);
  29. }
  30. }
  31. }

3.什么时候使用枚举类

任何时候使用枚举都需要一组常量,这些常量的成员在编译时已知。 当然,这包括“天然枚举类型”,如行星,星期几和棋子。 但是它也包含了其它你已经知道编译时所有可能值的集合,例如菜单上的选项,操作代码和命令行标志。一个枚举类型中的常量集不需要一直保持不变 枚举功能是专门设计用于允许二进制兼容的枚举类型的演变。
总结:枚举类型优于int常量的优点是令人信服的。 枚举更具可读性,更安全,更强大。 许多枚举不需要显式构造方法或成员,但其他人则可以通过将数据与每个常量关联并提供行为受此数据影响的方法而受益。 使用单一方法关联多个行为可以减少枚举。 在这种相对罕见的情况下,更喜欢使用常量特定的方法来枚举自己的值。 如果一些(但不是全部)枚举常量共享共同行为,请考虑策略枚举模式。