第30条:用枚举(Enum)来代替int常量

枚举类型(enum type)是指由一组固定的常量组成合法值的类型 ,例如一年中的季节、太阳系中的行星或者一副牌中的花色。

在编程语言还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的 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枚举模式(int enum pattern),存在着诸多不足。它在类型安全性和使用方便性方面没有任何帮助。如果你将 apple 传到想要 orange 的方法中,编译器也不会出现警告,还会使用 == 操作符将 apple 与 orange 进行对比,甚至更糟糕:

int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;

注意每个 apple 常量的名称都以 APPLE_ 作为前缀,每个 orange 常量则都以 ORANGE_ 作为前缀。这是因为 Java 没有为 int 枚举组提供命名空间。当两个 int 枚举组具有相同的明明常量时,前缀可以防止名称发生冲突。

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

将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。如果将这种常量打印出来,或者从调试器中将它显示出来,你所见到的就是一个数字,这没有太大的用处。要遍历一个组中的所有 int 枚举常量,甚至获得 int 枚举组的大小,这些都没有很可靠的方法。

你还可能碰到这种模式的变体,在这种模式中使用的是 String 常量,而不是 int 常量。这样的变体被称作 String 枚举模式,同样也是我们最不期望的。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作。更糟糕的是,它会导致初级用户把字符串创两硬编码到客户端代码中,而不是使用适当的域(field)名。如果这样的硬编码字符串常量中包含有书写错误,那么,这样的错误在编译时不会被检测到,但是在运行的时候却会报错。

一种可以替代的解决方案,可以避免 int 和 String 枚举模式的缺点,并提供许多额外的好处。这就是。下面以最简单的形式演示了这种模式:

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

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

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

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

枚举类型可以先作为枚举常量的一个简单集合,随着时间的推移再演变成为全功能的抽象。

下面这个枚举它们就是行星的质量和半径:

/**
 * 枚举 是指由一组固定的常量组成合法值得类型,int枚举和String枚举 不建议
 * 枚举的目的:
 *  通过公有的静态的final域 为每个枚举常量导出实例的类, 因为它没有访问的构造器,而且是final的,单例的泛型化。
 *  为了将数据和枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器 如下:
 *  劣势:与int常量相比,枚举在装载和初始化枚举时 会多一些 时间和空间成本。
 *  优势:枚举 代码优雅易读,安全性高,功能强大。(每个常量与属性的关联)(提供行为受属性影响的方法)
 *  如果多个常量共享相同的行为 可考虑策略枚举(枚举嵌套)如薪酬发放,各种加班行为的薪酬策略。
 *  应用方式:每当需要 一组 固定常量的时候,如 菜单的选项,操作代码,命令行标记,系统编码等等。
 */
public enum Planet {
    MERCURY(3.302e+23,2.439e6),
    VENUS(3.302e+23,2.439e6),
    EARTH(3.302e+23,2.439e6),
    MARS(3.302e+23,2.439e6),
    JUPITER(3.302e+23,2.439e6),
    SATURN(3.302e+23,2.439e6),
    URANUS(3.302e+23,2.439e6),
    NEPTUNE(3.302e+23,2.439e6);
    private final double mass;//值私有化 提供公有的提取方法。
    private final double radius;
    private final double surfaceGravity;
    private final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass/(radius * radius);
    }

    public double mass() {
        return mass;
    }

    public double radius() {
        return radius;
    }

    public double surfaceGravity() {
        return surfaceGravity;
    }
    public double surfaceWeight(double mass){
        return mass * surfaceGravity;
    }

}

特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难了。

一般来说,枚举会优先使用 comparable 而非 int 常量。与 int 常量相比,枚举有个小小的性能缺点,即装在和初始化枚举时会有空间和时间的成本。

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