使用枚举类型替代整型常量
枚举是其合法值由一组固定的常量组成的一种类型,例如一年中的季节,太阳系中的行星或一副扑
克牌中的花色。 在将枚举类型添加到该语言之前,表示枚举类型的常见模式是声明一组名为 int 的
常量,每个类型的成员都有一个常量:
// The int enum pattern - severely deficient! 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 ,或者更糟糕的是
// Tasty citrus flavored applesauce! int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
使用 int 枚举的程序很脆弱。 因为 int 枚举是编译时常量[JLS,4.12.4],所以它们的 int 值
被编译到使用它们的客户端中[JLS,13.1]。 如果与 int 枚举关联的值发生更改,则必须重新编译其客
户端。 如果没有,客户仍然会运行,但他们的行为将是不正确的
Java 提供了一种避免 int 和 String 枚举模式的所有缺点的替代方法,并提供了许
多额外的好处。 它是枚举类型[JLS,8.9]。 以下是它最简单的形式:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }
Java 枚举类型背后的基本思想很简单:它们是通过公共静态 final 属性为每个枚举常量导出一
个实例的类。 由于没有可访问的构造方法,枚举类型实际上是 final 的。 由于客户既不能创建枚举
类型的实例也不能继承它,除了声明的枚举常量外,不能有任何实例。 换句话说,枚举类型是实例控制
的(第 6 页)。 它们是单例(详见第 3 条)的泛型化,基本上是单元素的枚举
枚举提供了编译时类型的安全性。 如果声明一个参数为 Apple 类型,则可以保证传递给该参数
的任何非空对象引用是三个有效 Apple 值中的一个。 尝试传递错误类型的值将导致编译时错误,因
为会尝试将一个枚举类型的表达式分配给另一个类型的变量,或者使用 == 运算符来比较不同枚举类
型的值
具有相同名称常量的枚举类型可以和平共存,因为每种类型都有其自己的名称空间。 可以在枚举类
型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的属性在枚举类型与其客户端之间
提供了一层隔离:常量值不会编译到客户端,因为它们位于 int 枚举模式中。 最后,可以通过调用
其 toString 方法将枚举转换为可打印的字符串。
除了纠正 int 枚举的缺陷之外,枚举类型还允许添加任意方法和属性并实现任意接口。 它们提
供了所有 Object 方法的高质量实现,它们实现了 Comparable (详见第 14 条)和
Serializable (第 12 章),并针对枚举类型的可任意改变性设计了序列化方式
枚举本质上是不变的,所以所有的属
性都应该是 final 的(详见第 17 条)。 属性可以是公开的,但最好将它们设置为私有并提供公共访
问方法
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是
该顶级类的成员类(详见第 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; } t hrow 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); }
特定于常量的方法实现可以与特定于常量的数据结合使用。 例如,以下是 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("/") { 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); }
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)
一般而言,枚举通常在性能上与 int 常数相当。 枚举的一个小小的性能缺点是加载和初始化枚
举类型存在空间和时间成本,但在实践中不太可能引人注意。
那么你应该什么时候使用枚举呢? 任何时候使用枚举都需要一组常量,这些常量的成员在编译时已
知。 当然,这包括“天然枚举类型”,如行星,星期几和棋子。 但是它也包含了其它你已经知道编译时
所有可能值的集合,例如菜单上的选项,操作代码和命令行标志。 一个枚举类型中的常量集不需要一
直保持不变。 枚举功能是专门设计用于允许二进制兼容的枚举类型的演变。
总之,枚举类型优于 int 常量的优点是令人信服的。 枚举更具可读性,更安全,更强大。 许多
枚举不需要显式构造方法或成员,但其他人则可以通过将数据与每个常量关联并提供行为受此数据影响
的方法而受益。 使用单一方法关联多个行为可以减少枚举。 在这种相对罕见的情况下,更喜欢使用常
量特定的方法来枚举自己的值。 如果一些(但不是全部)枚举常量共享共同行为,请考虑策略枚举模
式