枚举类型是 JDK 5 之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量。
在没有引入 enum 关键字之前,要表示可枚举的变量,只能使用 public static final 的方式。
public staic final int SPRING = 1;public staic final int SUMMER = 2;public staic final int AUTUMN = 3;public staic final int WINTER = 4;复制代码
这种实现方式有几个弊端。首先,类型不安全。试想一下,有一个方法期待接受一个季节作为参数,那么只能将参数类型声明为 int,但是传入的值可能是 99。显然只能在运行时进行参数合理性的判断,无法在编译期间完成检查。其次,指意性不强,含义不明确。我们使用枚举,很多场合会用到该枚举的字串符表达,而上述的实现中只能得到一个数字,不能直观地表达该枚举常量的含义。当然也可用 String 常量,但是又会带来性能问题,因为比较要依赖字符串的比较操作。
使用 enum 来表示枚举可以更好地保证程序的类型安全和可读性。
enum 是类型安全的。除了预先定义的枚举常量,不能将其它的值赋给枚举变量。这和用 int 或 String 实现的枚举很不一样。
enum 有自己的名称空间,且可读性强。在创建 enum 时,编译器会自动添加一些有用的特性。每个 enum 实例都有一个名字 (name) 和一个序号 (ordinal),可以通过 toString() 方法获取 enum 实例的字符串表示。还以通过 values() 方法获得一个由 enum 常量按顺序构成的数组。
enum 还有一个特别实用的特性,可以在 switch 语句中使用,这也是 enum 最常用的使用方式了。
反编译枚举类型源码
public enum Season { SPRING, SUMMER, AUTUMN, WINTER;}复制代码
用 javap 反编译一下生成的 class 文件:
public final class Season extends java.lang.Enum{ public static final Season SPRING; public static final Season SUMMER; public static final Season AUTUMN; public static final Season WINTER; public static Season[] values(); public static Season valueOf(java.lang.String); static {};}复制代码
可以看到,实际上在经过编译器编译后生成了一个 Season 类,该类继承自 Enum 类,且是 final 的。从这一点来看,Java 中的枚举类型似乎就是一个语法糖。
每一个枚举常量都对应类中的一个 public static final 的实例,这些实例的初始化应该是在 static {} 语句块中进行的。因为枚举常量都是 final 的,因而一旦创建之后就不能进行更改了。 此外,Season 类还实现了 values() 和 valueOf() 这两个静态方法。
再用 jad 进行反编译,我们可以大致看到 Season 类内部的实现细节:
public final class Season extends Enum{ public static Season[] values() { return (Season[])$VALUES.clone(); } public static Season valueOf(String s) { return (Season)Enum.valueOf(Season, s); } private Season(String s, int i) { super(s, i); } public static final Season SPRING; public static final Season SUMMER; public static final Season AUTUMN; public static final Season WINTER; private static final Season $VALUES[]; static { SPRING = new Season("SPRING", 0); SUMMER = new Season("SUMMER", 1); AUTUMN = new Season("AUTUMN", 2); WINTER = new Season("WINTER", 3); $VALUES = (new Season[] { SPRING, SUMMER, AUTUMN, WINTER }); }}复制代码
除了对应的四个枚举常量外,还有一个私有的数组,数组中的元素就是枚举常量。编译器自动生成了一个 private 的构造方法,这个构造方法中直接调用父类的构造方法,传入了一个字符串和一个整型变量。从初始化语句中可以看到,字符串的值就是声明枚举常量时使用的名称,而整型变量分别是它们的顺序(从0开始)。枚举类的实现使用了一种多例模式,只有有限的对象可以创建,无法显示调用构造方法创建对象。
values() 方法返回枚举常量数组的一个浅拷贝,可以通过这个数组访问所有的枚举常量;而 valueOf() 则直接调用父类的静态方法 Enum.valueOf(),根据传入的名称字符串获得对应的枚举对象。
Enum 类是不能被继承的,如果我们按照上面反编译的结果自己写一个这样的实现,是不能编译成功的。Java 编译器限制了我们显式的继承 java.Lang.Enum 类, 报错 The type may not subclass Enum explicitly。
Enum 类源码
上面反编译出的 Season 类继承自 Enum 类,且调用了父类的构造函数和静态方法 valueOf,我们来通过 Enum 源码看一下其内部的实现。
public abstract class Enum> implements Comparable , Serializable {}复制代码
从类的声明来看,Enum 是个抽象类,且用到了泛型,类型参数的值必须要是 Enum 的子类。Enum 类还实现了 Comparable 和 Serializable 接口。
Enum 类有两个私有的成员,name 和 ordinal,在 protected 的构造方法中初始化,分别是表示枚举常量名称的字符串、枚举常量在枚举定义中序号的整型变量。这两个常量当然也会被其子类继承,前面看到 Season 类的构造方法中就是直接调用父类的构造方法设置这两个成员的。name 和 ordinal 都是 final 修饰的,一旦初始化后就不能进行修改了。
private final String name;public final String name() { return name;}private final int ordinal; //从0开始public final int ordinal() { return ordinal;}protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal;}复制代码
toString() 方法默认返回枚举常量的名称,该方法在子类中可以进行重写。
public String toString() { return name;}复制代码
前面我们看到,编译后生成的枚举类中使用了多例模式,其对象只有有限个,且一旦创建后就不能修改。因为每一个枚举常量总是单例的,因而可以使用 == 直接进行比较。equals() 方法就是直接使用 == 比较两个枚举类型的变量的。
//直接使用 `==` 比较,不可在子类重写public final boolean equals(Object other) { return this==other;}// 返回该枚举常量的哈希码。和equals一致,该方法不可以被重写。 public final int hashCode() { return super.hashCode();} //类型要相同,根据它们在枚举声明中的先后顺序来返回大小(前面的小,后面的大)。//子类不可以重写该方法 public final int compareTo(E o) { Enum other = (Enum )o; Enumself = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal - other.ordinal;}复制代码
valueOf() 方法根据传入的字符串返回对应名称的枚举常量。调用 Class 对象的 enumConstantDirectory() (package-private)方法会创建一个名称和枚举常量的 Map,然后以名称为键进行查找。这里名称必须和枚举声明时的名称完全一致。
//返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。public static> T valueOf(Class enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name);}// 得到枚举常量所属枚举类型的Class对象 public final Class getDeclaringClass() { Class clazz = getClass(); Class zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? clazz : zuper; }复制代码
枚举对象不能序列化和反序列化,也不允许克隆:
protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException();}/** * enum classes cannot have finalize methods. */protected final void finalize() { }/** * prevent default deserialization */private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("can't deserialize enum");}private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("can't deserialize enum");}复制代码
使用枚举
Java 中的枚举相较于其它编程语言要强大许多,这里给出几个枚举的简单用法。关于枚举的使用,在「Java 编程思想」中有十分详细的说明。
作为常量
enum Color { RED, GREEN, BLUE}class Test { public void main(String[] args) { Color color = Color.RED; switch(color) { case RED: System.out.println("red"); break; case GREEN: System.out.println("green"); break; case GREEN: System.out.println("blue"); break; default: System.out.println("unknow"); } }}复制代码
向枚举中添加成员和方法
除了不能继承自 enum 外,我们基本可以将 enum 看作一个普通的类。可以向 enum 中添加成员和方法。添加成员则必须要有对应的构造函数,成员和构造函数都隐式强制为私有的,不可以从外部调用。方法则和普通类中的方法一致。
public enum Planet { MERCURY (3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6), JUPITER (1.9e+27, 7.1492e7), SATURN (5.688e+26, 6.0268e7), URANUS (8.686e+25, 2.5559e7), NEPTUNE (1.024e+26, 2.4746e7); private final double mass; // in kilograms private final double radius; // in meters Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } private double mass() { return mass; } private double radius() { return radius; } // universal gravitational constant (m3 kg-1 s-2) public static final double G = 6.67300E-11; double surfaceGravity() { return G * mass / (radius * radius); } double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } public static void main(String[] args) { if (args.length != 1) { System.err.println("Usage: java Planet"); System.exit(-1); } double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight/EARTH.surfaceGravity(); for (Planet p : Planet.values()) System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass)); }}复制代码
反编译后发现构造方法如下:
private Planet(String s, int i, double d, double d1){ super(s, i); mass = d; radius = d1;}复制代码
为个别枚举常量重写方法
上一个例子中的为枚举新添加的方法是所有枚举实例共有的,实际上还可以为个别枚举实例单独重写方法:
public enum Gender { Male { @Override public void behave() { System.out.println("Male special behavior"); } }, Female; @Override public void behave() { System.out.println("Common behavior"); } public static void main(String[] args) { Gender.Male.behave(); //Male special behavior Gender.Female.behave(); //Common behavior }}复制代码
这里实际上产生了一个 Gender 的匿名子类 Gender$1,子类重写了父类 Gender 的 behave() 方法。在 Gender 的初始化语句中,生成枚举实例时:
public static final Male = new Gender$1("Male", 0);public static final Female = new Gender("Female", 1);复制代码
实现接口
枚举类默认继承自 Enum,由于不支持多继承,无法再继承其它类,但还可以实现接口。
interface Behavior { void behave();}public enum Gender implements Behavior { Male { @Override public void behave() { System.out.println("Male special behavior"); } }, Female; @Override public void behave() { System.out.println("Common behavior"); } public static void main(String[] args) { Gender.Male.behave(); //Male special behavior Gender.Female.behave(); //Common behavior Behavior man = Gender.Male; man.behave(); }}复制代码
用枚举实现单例
写一个线程安全的单例模式并不是一个简单的事情,但是借助枚举我们可以轻易地做到这一点。还记得我们前面分析枚举的源码时的发现吗? 每一个枚举常量在枚举类中的定义都是 public static final。static 类型保证了该实例会在类加载时被初始化(Java 的类加载机制,类第一次被使用时加载静态资源),且类加载和初始化是线程安全的。 final 则可以保证一旦完成初始化,该常量将不能被更改。 此外,由于父类 Enum 中明确了枚举对象不能被序列化和反序列化。 枚举完美地解决了单例模式中的序列化和反序列化问题,使用起来也极其简单:
public enum Singleton { INSTANCE; public void doSomething() { //todo }}复制代码
使用接口组织枚举
public interface Food { enum Coffee implements Food{ BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO } enum Dessert implements Food{ FRUIT, CAKE, GELATO }}复制代码
EnumSet 和 EnumMap
EnumSet 是一个特殊的 Set,其内部的元素必须是来自同一个 enum。EnumSet 内部使用 bit 向量实现,这种实现方式更紧凑高效,类似于传统基于 int 的位标志。相比于位标志,EnumSet 的可读性更强,且性能上也相差不大。详细可参考官方 API。
EnumMap 是一种特殊的 Map,要求其中的键 (key) 必须来自于同一个 enum。由于 enum 自身的实例数量是有限的,EnumMap 在内部可由数组实现,因此速度很快。除了只能使用 enum 作为键以外,其它的操作和一般的 Map 没有太大区别。详细可参考官方 API。