引言
枚举常被用来表示数量有限且固定的事物,例如四个季节、性别、城市等。它天然的不可变性带来了很多使用上的方便。从编译器的角度来说,枚举也可以理解成一种语法糖,因为编译器将枚举类编译后的文件与实际的代码有很大差别,这篇文章我们就从更深入的层次探究一下java中枚举的实现。
枚举的声明
枚举的声明很简单,类似下面的,就是一个很简单的枚举:
public enum Day {
SUNDAY,MONDAY;
}
源码分析
枚举类的源码再简单不过了,编译器为我们隐藏了很多细节,我们需要首先解语法糖。
下面是通过jad -sjava Day.class反编译得到的文件:
package person.andy.concurrency.eu;
public final class Day extends Enum
{
public static Day[] values()
{
return (Day[])$VALUES.clone();
}
public static Day valueOf(String name)
{
return (Day)Enum.valueOf(person/andy/concurrency/eu/day, name);
}
private Day(String s, int i)
{
super(s, i);
}
public static final Day SUNDAY;
public static final Day MONDAY;
private static final Day $VALUES[];
static
{
SUNDAY = new Day("SUNDAY", 0);
MONDAY = new Day("MONDAY", 1);
$VALUES = (new Day[] {
SUNDAY, MONDAY
});
}
}
在反编译的类中,编译器自动帮我们生成了一份真正在jvm中运行的代码。
首先,我们的Day类自动继承了java.lang.Enum类,并且增加了final修饰符,也就是说这个类是不能被继承的。
Enum类的实现
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable
可以看到Enum是一个抽象类,实现了Comparable和Serializable,也就是说Enum是可以进行排序的。具体怎么比较等下面分析方法的时候我们就会看到。
字段和方法
Enum类中有两个重要的字段,即name和ordinal,分别是string和int类型,这两个字段有什么用呢?
private final String name;
private final int ordinal;
通俗的解释一下就是,我们声明的Enum常量,也就是Day中的SUNDAY和MONDAY,每个都会有name和ordinal这两个属性,name就是我们SUNDAY和MONDAY这两个字符串,ordinal就是每个常量在常量声明中的顺序,第一个声明的常量的ordinal就是0,以此类推。
有了这两个基本的属性,就会有构造方法,Enum的构造方法如下:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
可以看到构造方法就是用了name和ordinal这两个属性。这里还需要注意一点就是构造方法是protected的,也就是只在子类中可以使用。
在反编译的Day类中,我们可以发现编译器为Day自动生成的构造方法:
private Day(String s, int i)
{
super(s, i);
}
注意这是一个私有方法,也就是外部类不能实例化Day的对象。而构造方法内部就是调用了Enum的构造方法来为name和ordinal两个成员变量来赋值。
那为什么构造方法是私有的呢?因为枚举类必须保证它的实例是固定的,不能随意生成。
另外Enum类实现了Clone方法,Enum的clone方法实现如下:
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
直接抛出了异常,也就是枚举类是不能被clone的。并且加上了protected final修饰符,说明这个方法在子类中不能被重写,也就是每个枚举类都是不能clone的。
Enum类实现了Comparable接口,支持排序,可以通过Collections.sort进行自动排序。该类实现了compareTo(E o)接口,方法定义为final说明不能被重写,在方法的实现中:
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;
}
是按照oridinal字段进行排序的,所以所有的枚举类都只能根据oridinal也就是在枚举常量中定义的顺序来进行排序。
再看反编译得到的Day文件,我们声明的每个枚举变量SUNDAY和MONDAY都被编译器自动声明为一个static final的Day类型变量,并且在static块中进行了初始化,同时,还生成了一个Day数组类型的value字段,同样是在static块中进行了初始化,初始化的方法就是将每个初始化的成员变量作为数组的元素。
public static final Day SUNDAY;
public static final Day MONDAY;
private static final Day $VALUES[];
static
{
SUNDAY = new Day("SUNDAY", 0);
MONDAY = new Day("MONDAY", 1);
$VALUES = (new Day[] {
SUNDAY, MONDAY
});
}
将每个枚举常量声明为static final并且在static块中进行初始化,保证了每个枚举常量只有一个实例和线程安全性。
Day的values方法的实现也需要注意:
public static Day[] values()
{
return (Day[])$VALUES.clone();
}
并没有直接返回values,而是返回了values的一份副本,这是为了防止直接返回values导致枚举常量被外面的类访问到而进行修改,就不能确保枚举常量的不变性,也就是说,避免了不安全的发布。
接下来再看一个重要的方法,valueOf,我们看到Day反编译后的valueOf方法是这样的:
public static Day valueOf(String name)
{
return (Day)Enum.valueOf(person/andy/concurrency/eu/day, name);
}
注意这是一个static方法,只有一个name参数,内部是调用了Enum的valueOf方法,所以Enum的valueOf方法是重点:
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);
}
Enum的valueOf同样是static方法,有两个参数,一个代表枚举类的class对象,一个是name。
它里面实际上用的是这个class对象的enumConstantDirectory()方法,下面看这个方法的实现:
/**
* Returns a map from simple name to enum constant. This package-private
* method is used internally by Enum to implement
* {@code public static <T extends Enum<T>> T valueOf(Class<T>, String)}
* efficiently. Note that the map is returned by this method is
* created lazily on first use. Typically it won't ever get created.
*/
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
在这个方法里面,首先调用了getEnumConstantsShared()方法来获得该class的所有枚举常量,然后新声明了一个Map
T[] getEnumConstantsShared() {
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex) { return null; }
}
return enumConstants;
}
private volatile transient T[] enumConstants = null;
很清楚,调用的是枚举类的values方法,我们应该还记得Day反编译出来的代码中有一个values方法,该方法返回的是values字段的副本,也就是该枚举的所有枚举常量。也就是说,最终获得所有的枚举常量还是通过Day这个枚举类自身来实现的。
所以针对Day的valueOf方法可以总结出这样的流程:
每一个枚举类型及其定义的枚举变量在jvm中都是唯一的。这个结果通过以下几个手段来保证:
(1)类加载时创建枚举常量的实例,保证线程安全。
Day类的两个枚举常量SUNDAY和MONDAY都是final static的,并且在static块中进行初始化,对jvm来说,也就是
(2)对序列化进行特殊处理,防止反序列化时创建新的对象。
我们知道一旦实现了Serializable接口之后,反序列化时每次调用readObject()方法返回的都是一个新创建出来的对象。而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器不允许任何对这种序列化进行定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
/**
* 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");
}
(3)私有构造函数,无法通过new来创建对象。
private day(String s, int i)
{
super(s, i);
}
(4)无法通过clone方法,克隆对象。
/**
* Throws CloneNotSupportedException. This guarantees that enums
* are never cloned, which is necessary to preserve their "singleton"
* status.
*
* @return (never returns)
*/
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
(5)无法通过反射的方式创建枚举对象。
参考下面的示例:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> dayClass = Class.forName("person.andy.concurrency.eu.Day");
Constructor<?> declaredConstructor = dayClass.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
declaredConstructor.newInstance("TUESDAY",2);
}
执行会报错:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at person.andy.concurrency.eu.Day.main(Day.java:13)
我们去看Constructor.newInstance方法,会看到这样的判断:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
所以,通过反射来构造枚举实例的行为也是被禁止的。
枚举类的特点总结:
(1)枚举实例必须在enum关键字声明的类中显式的指定(首行开始的以第一个分号结束)。
(2)除了1,没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例。
(3)枚举类不可被继承。
(4)枚举类是线程安全的。
(5)枚举类型是类型安全的。
(6)无法继承其他类(已经默认继承Enum)。
带有额外成员变量的情况
我们上面看到,当声明一个类似于Day这样的枚举类时,编译器会为其自动生成一个私有构造方法,该构造方法的参数是name和ordinal,并且是通过调用父类Enum的构造方法来实现的。
private Day(String s, int i)
{
super(s, i);
}
如果我们声明的枚举有额外的成员变量呢,类似下面的示例:
public enum Day {
SUNDAY("sunday"),MONDAY("monday");
private String cost;
Day(String cost) {
this.cost = cost;
}
}
我们增加了cost成员变量,并且在一个构造方法中使用了这个成员变量。注意这个构造方法没有任何修饰符,编译器会自动为它加上private限制。
还是直接查看jad反编译的代码:
package person.andy.concurrency.eu;
public final class Day extends Enum
{
public static Day[] values()
{
return (Day[])$VALUES.clone();
}
public static Day valueOf(String name)
{
return (Day)Enum.valueOf(person/andy/concurrency/eu/Day, name);
}
private Day(String s, int i, String cost)
{
super(s, i);
this.cost = cost;
}
public static final Day SUNDAY;
public static final Day MONDAY;
private String cost;
private static final Day $VALUES[];
static
{
SUNDAY = new Day("SUNDAY", 0, "sunday");
MONDAY = new Day("MONDAY", 1, "monday");
$VALUES = (new Day[] {
SUNDAY, MONDAY
});
}
}
可以发现,编译器自动生成了新的构造方法,该构造方法有3个参数,其中一个是cost,而另外两个就是name和ordinal,这两个的初始化是通过Enum的构造方法来实现的。并且这个构造方法是私有的。
关于枚举类的Finalize方法
Enum类中重写了finalize方法,但是是个空实现,并且不能被子类继承,也就是说枚举类不能有finalize方法,这样做是为了保证枚举类实例在jvm中是一直存在的。
/**
* enum classes cannot have finalize methods.
*/
protected final void finalize() { }
小结
枚举作为语法糖的一种,比自动装箱、拆箱要复杂一点,但是相比如泛型,就简单很多了。我们直接查看编译器生成的文件就能理解很多东西。有了上面对枚举的理解,我们就能知道在哪些情况下使用枚举,也更能知道自己在创建一个枚举时发生了什么。