如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(条目 24)。 例如,java.math.RoundingMode枚举表示小数部分的舍入模式。 BigDecimal类使用了这些舍入模式,但它们提供了一种有用的抽象,它并不与BigDecimal有根本的联系。 通过将RoundingMode设置为顶层枚举,类库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨API的一致性。
// Enum type that switches on its own value - questionable public enum Operation { PLUS, MINUS, TIMES, DIVIDE; // Do the arithmetic operation represented by this constant public double apply(double x, double y) { switch(this) { case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y; } throw new AssertionError("Unknown op: " + this); } }此代码有效,但不是很漂亮。 如果没有throw语句,就不能编译,因为该方法的结束在技术上是可达到的,尽管它永远不会被达到[JLS,14.21]。 更糟的是,代码很脆弱。 如果添加新的枚举常量,但忘记向switch语句添加相应的条件,枚举仍然会编译,但在尝试应用新操作时,它将在运行时失败。
幸运的是,有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的方法实现:
// Enum type with constant-specific method implementations public enum 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;}}; public abstract double apply(double x, double y); }如果向第二个版本的操作添加新的常量,则不太可能会忘记提供apply方法,因为该方法紧跟在每个常量声明之后。 万一忘记了,编译器会提醒你,因为枚举类型中的抽象方法必须被所有常量中的具体方法重写。
特定于常量的方法实现可以与特定于常量的数据结合使用。 例如,以下是Operation的一个版本,它重写toString方法以返回通常与该操作关联的符号:
// Enum type with constant-specific class bodies and data public enum 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("http://www.likecs.com/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public abstract double apply(double x, double y); }显示的toString实现可以很容易地打印算术表达式,正如这个小程序所展示的那样:
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)); }以2和4作为命令行参数运行此程序会生成以下输出:
2.000000 + 4.000000 = 6.000000 2.000000 - 4.000000 = -2.000000 2.000000 * 4.000000 = 8.000000 2.000000 / 4.000000 = 0.500000枚举类型具有自动生成的valueOf(String)方法,该方法将常量名称转换为常量本身。 如果在枚举类型中重写toString方法,请考虑编写fromString方法将自定义字符串表示法转换回相应的枚举类型。 下面的代码(类型名称被适当地改变)将对任何枚举都有效,只要每个常量具有唯一的字符串表示形式:
// Implementing a fromString method on an enum type private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect( toMap(Object::toString, e -> e)); // Returns Operation for string, if any public static Optional<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); }请注意,Operation枚举常量被放在stringToEnum的map中,它来自于创建枚举常量后运行的静态属性初始化。前面的代码在values()方法返回的数组上使用流(第7章);在Java 8之前,我们创建一个空的hashMap并遍历值数组,将字符串到枚举映射插入到map中,如果愿意,仍然可以这样做。但请注意,尝试让每个常量都将自己放入来自其构造方法的map中不起作用。这会导致编译错误,这是好事,因为如果它是合法的,它会在运行时导致NullPointerException。除了编译时常量属性(条目 34)之外,枚举构造方法不允许访问枚举的静态属性。此限制是必需的,因为静态属性在枚举构造方法运行时尚未初始化。这种限制的一个特例是枚举常量不能从构造方法中相互访问。