枚举类型是 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<Season> {
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<E extends Enum<E>>
implements Comparable<E>, 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;
Enum<E> self = 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 extends Enum<T>> T valueOf(Class<T> 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<E> 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 <earth_weight>");
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。