引言
枚举类型是 JDK 5 之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量。
在没有引入 enum 关键字之前,要表示可枚举的变量,只能使用 public static final
的方式。
1
2
3
4
|
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 最常用的使用方式了。
下面我们从源码方面分析一下 enum 的实现方式,并介绍几种 enum 的用法。
反编译枚举类型源码
1
2
3
|
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
|
用 javap 反编译一下生成的 class 文件:
1
2
3
4
5
6
7
8
9
|
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 类内部的实现细节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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 源码看一下其内部的实现。
1
2
|
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 修饰的,一旦初始化后就不能进行修改了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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()
方法默认返回枚举常量的名称,该方法在子类中可以进行重写。
1
2
3
|
public String toString() {
return name;
}
|
前面我们看到,编译后生成的枚举类中使用了多例模式,其对象只有有限个,且一旦创建后就不能修改。因为每一个枚举常量总是单例的,因而可以使用 ==
直接进行比较。equals()
方法就是直接使用 ==
比较两个枚举类型的变量的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//直接使用 `==` 比较,不可在子类重写
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,然后以名称为键进行查找。这里名称必须和枚举声明时的名称完全一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。
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;
}
|
枚举对象不能序列化和反序列化,也不允许克隆:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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 编程思想」中有十分详细的说明。
作为常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 中添加成员和方法。添加成员则必须要有对应的构造函数,成员和构造函数都隐式强制为私有的,不可以从外部调用。方法则和普通类中的方法一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
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));
}
}
|
反编译后发现构造方法如下:
1
2
3
4
5
6
|
private Planet(String s, int i, double d, double d1)
{
super(s, i);
mass = d;
radius = d1;
}
|
为个别枚举常量重写方法
上一个例子中的为枚举新添加的方法是所有枚举实例共有的,实际上还可以为个别枚举实例单独重写方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 的初始化语句中,生成枚举实例时:
1
2
|
public static final Male = new Gender$1("Male", 0);
public static final Female = new Gender("Female", 1);
|
实现接口
枚举类默认继承自 Enum,由于不支持多继承,无法再继承其它类,但还可以实现接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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 中明确了枚举对象不能被序列化和反序列化。
枚举完美地解决了单例模式中的序列化和反序列化问题,使用起来也极其简单:
1
2
3
4
5
6
|
public enum Singleton {
INSTANCE;
public void doSomething() {
//todo
}
}
|
使用接口组织枚举
1
2
3
4
5
6
7
8
|
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。
-EOF-
参考
通过反编译字节码来理解 Java 枚举
[转+注]单例模式的七种写法