引言

枚举常被用来表示数量有限且固定的事物,例如四个季节、性别、城市等。它天然的不可变性带来了很多使用上的方便。从编译器的角度来说,枚举也可以理解成一种语法糖,因为编译器将枚举类编译后的文件与实际的代码有很大差别,这篇文章我们就从更深入的层次探究一下java中枚举的实现。

枚举的声明

枚举的声明很简单,类似下面的,就是一个很简单的枚举:

  1. public enum Day {
  2. SUNDAY,MONDAY;
  3. }

源码分析

枚举类的源码再简单不过了,编译器为我们隐藏了很多细节,我们需要首先解语法糖。
下面是通过jad -sjava Day.class反编译得到的文件:

  1. package person.andy.concurrency.eu;
  2. public final class Day extends Enum
  3. {
  4. public static Day[] values()
  5. {
  6. return (Day[])$VALUES.clone();
  7. }
  8. public static Day valueOf(String name)
  9. {
  10. return (Day)Enum.valueOf(person/andy/concurrency/eu/day, name);
  11. }
  12. private Day(String s, int i)
  13. {
  14. super(s, i);
  15. }
  16. public static final Day SUNDAY;
  17. public static final Day MONDAY;
  18. private static final Day $VALUES[];
  19. static
  20. {
  21. SUNDAY = new Day("SUNDAY", 0);
  22. MONDAY = new Day("MONDAY", 1);
  23. $VALUES = (new Day[] {
  24. SUNDAY, MONDAY
  25. });
  26. }
  27. }

在反编译的类中,编译器自动帮我们生成了一份真正在jvm中运行的代码。
首先,我们的Day类自动继承了java.lang.Enum类,并且增加了final修饰符,也就是说这个类是不能被继承的。

Enum类的实现

  1. public abstract class Enum<E extends Enum<E>>
  2. implements Comparable<E>, Serializable

可以看到Enum是一个抽象类,实现了Comparable和Serializable,也就是说Enum是可以进行排序的。具体怎么比较等下面分析方法的时候我们就会看到。

字段和方法

Enum类中有两个重要的字段,即name和ordinal,分别是string和int类型,这两个字段有什么用呢?

  1. private final String name;
  2. private final int ordinal;

通俗的解释一下就是,我们声明的Enum常量,也就是Day中的SUNDAY和MONDAY,每个都会有name和ordinal这两个属性,name就是我们SUNDAY和MONDAY这两个字符串,ordinal就是每个常量在常量声明中的顺序,第一个声明的常量的ordinal就是0,以此类推。
有了这两个基本的属性,就会有构造方法,Enum的构造方法如下:

  1. protected Enum(String name, int ordinal) {
  2. this.name = name;
  3. this.ordinal = ordinal;
  4. }

可以看到构造方法就是用了name和ordinal这两个属性。这里还需要注意一点就是构造方法是protected的,也就是只在子类中可以使用。
在反编译的Day类中,我们可以发现编译器为Day自动生成的构造方法:

  1. private Day(String s, int i)
  2. {
  3. super(s, i);
  4. }

注意这是一个私有方法,也就是外部类不能实例化Day的对象。而构造方法内部就是调用了Enum的构造方法来为name和ordinal两个成员变量来赋值。
那为什么构造方法是私有的呢?因为枚举类必须保证它的实例是固定的,不能随意生成。
另外Enum类实现了Clone方法,Enum的clone方法实现如下:

  1. protected final Object clone() throws CloneNotSupportedException {
  2. throw new CloneNotSupportedException();
  3. }

直接抛出了异常,也就是枚举类是不能被clone的。并且加上了protected final修饰符,说明这个方法在子类中不能被重写,也就是每个枚举类都是不能clone的。
Enum类实现了Comparable接口,支持排序,可以通过Collections.sort进行自动排序。该类实现了compareTo(E o)接口,方法定义为final说明不能被重写,在方法的实现中:

  1. public final int compareTo(E o) {
  2. Enum<?> other = (Enum<?>)o;
  3. Enum<E> self = this;
  4. if (self.getClass() != other.getClass() && // optimization
  5. self.getDeclaringClass() != other.getDeclaringClass())
  6. throw new ClassCastException();
  7. return self.ordinal - other.ordinal;
  8. }

是按照oridinal字段进行排序的,所以所有的枚举类都只能根据oridinal也就是在枚举常量中定义的顺序来进行排序。
再看反编译得到的Day文件,我们声明的每个枚举变量SUNDAY和MONDAY都被编译器自动声明为一个static final的Day类型变量,并且在static块中进行了初始化,同时,还生成了一个Day数组类型的value字段,同样是在static块中进行了初始化,初始化的方法就是将每个初始化的成员变量作为数组的元素。

  1. public static final Day SUNDAY;
  2. public static final Day MONDAY;
  3. private static final Day $VALUES[];
  4. static
  5. {
  6. SUNDAY = new Day("SUNDAY", 0);
  7. MONDAY = new Day("MONDAY", 1);
  8. $VALUES = (new Day[] {
  9. SUNDAY, MONDAY
  10. });
  11. }

将每个枚举常量声明为static final并且在static块中进行初始化,保证了每个枚举常量只有一个实例和线程安全性。
Day的values方法的实现也需要注意:

  1. public static Day[] values()
  2. {
  3. return (Day[])$VALUES.clone();
  4. }

并没有直接返回values,而是返回了values的一份副本,这是为了防止直接返回values导致枚举常量被外面的类访问到而进行修改,就不能确保枚举常量的不变性,也就是说,避免了不安全的发布。
接下来再看一个重要的方法,valueOf,我们看到Day反编译后的valueOf方法是这样的:

  1. public static Day valueOf(String name)
  2. {
  3. return (Day)Enum.valueOf(person/andy/concurrency/eu/day, name);
  4. }

注意这是一个static方法,只有一个name参数,内部是调用了Enum的valueOf方法,所以Enum的valueOf方法是重点:

  1. public static <T extends Enum<T>> T valueOf(Class<T> enumType,
  2. String name) {
  3. T result = enumType.enumConstantDirectory().get(name);
  4. if (result != null)
  5. return result;
  6. if (name == null)
  7. throw new NullPointerException("Name is null");
  8. throw new IllegalArgumentException(
  9. "No enum constant " + enumType.getCanonicalName() + "." + name);
  10. }

Enum的valueOf同样是static方法,有两个参数,一个代表枚举类的class对象,一个是name。
它里面实际上用的是这个class对象的enumConstantDirectory()方法,下面看这个方法的实现:

  1. /**
  2. * Returns a map from simple name to enum constant. This package-private
  3. * method is used internally by Enum to implement
  4. * {@code public static <T extends Enum<T>> T valueOf(Class<T>, String)}
  5. * efficiently. Note that the map is returned by this method is
  6. * created lazily on first use. Typically it won't ever get created.
  7. */
  8. Map<String, T> enumConstantDirectory() {
  9. if (enumConstantDirectory == null) {
  10. T[] universe = getEnumConstantsShared();
  11. if (universe == null)
  12. throw new IllegalArgumentException(
  13. getName() + " is not an enum type");
  14. Map<String, T> m = new HashMap<>(2 * universe.length);
  15. for (T constant : universe)
  16. m.put(((Enum<?>)constant).name(), constant);
  17. enumConstantDirectory = m;
  18. }
  19. return enumConstantDirectory;
  20. }
  21. private volatile transient Map<String, T> enumConstantDirectory = null;

在这个方法里面,首先调用了getEnumConstantsShared()方法来获得该class的所有枚举常量,然后新声明了一个Map,之后将每个枚举常量存入map,枚举常量的name作为key,该常量自己作为value。那getEnumConstantsShared()方法是怎样得到所有的枚举常量呢?直接看源码:

  1. T[] getEnumConstantsShared() {
  2. if (enumConstants == null) {
  3. if (!isEnum()) return null;
  4. try {
  5. final Method values = getMethod("values");
  6. java.security.AccessController.doPrivileged(
  7. new java.security.PrivilegedAction<Void>() {
  8. public Void run() {
  9. values.setAccessible(true);
  10. return null;
  11. }
  12. });
  13. @SuppressWarnings("unchecked")
  14. T[] temporaryConstants = (T[])values.invoke(null);
  15. enumConstants = temporaryConstants;
  16. }
  17. // These can happen when users concoct enum-like classes
  18. // that don't comply with the enum spec.
  19. catch (InvocationTargetException | NoSuchMethodException |
  20. IllegalAccessException ex) { return null; }
  21. }
  22. return enumConstants;
  23. }
  24. private volatile transient T[] enumConstants = null;

很清楚,调用的是枚举类的values方法,我们应该还记得Day反编译出来的代码中有一个values方法,该方法返回的是values字段的副本,也就是该枚举的所有枚举常量。也就是说,最终获得所有的枚举常量还是通过Day这个枚举类自身来实现的。
所以针对Day的valueOf方法可以总结出这样的流程:

Enum valueOf方法调用流程图.png

每一个枚举类型及其定义的枚举变量在jvm中都是唯一的。这个结果通过以下几个手段来保证:
(1)类加载时创建枚举常量的实例,保证线程安全。
Day类的两个枚举常量SUNDAY和MONDAY都是final static的,并且在static块中进行初始化,对jvm来说,也就是方法,这样保证了初始化是线程安全的并且初始化之后具有不可变性。
(2)对序列化进行特殊处理,防止反序列化时创建新的对象。
我们知道一旦实现了Serializable接口之后,反序列化时每次调用readObject()方法返回的都是一个新创建出来的对象。而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器不允许任何对这种序列化进行定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

  1. /**
  2. * prevent default deserialization
  3. */
  4. private void readObject(ObjectInputStream in) throws IOException,
  5. ClassNotFoundException {
  6. throw new InvalidObjectException("can't deserialize enum");
  7. }
  8. private void readObjectNoData() throws ObjectStreamException {
  9. throw new InvalidObjectException("can't deserialize enum");
  10. }

(3)私有构造函数,无法通过new来创建对象。

  1. private day(String s, int i)
  2. {
  3. super(s, i);
  4. }

(4)无法通过clone方法,克隆对象。

  1. /**
  2. * Throws CloneNotSupportedException. This guarantees that enums
  3. * are never cloned, which is necessary to preserve their "singleton"
  4. * status.
  5. *
  6. * @return (never returns)
  7. */
  8. protected final Object clone() throws CloneNotSupportedException {
  9. throw new CloneNotSupportedException();
  10. }

(5)无法通过反射的方式创建枚举对象。
参考下面的示例:

  1. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
  2. Class<?> dayClass = Class.forName("person.andy.concurrency.eu.Day");
  3. Constructor<?> declaredConstructor = dayClass.getDeclaredConstructor(String.class, int.class);
  4. declaredConstructor.setAccessible(true);
  5. declaredConstructor.newInstance("TUESDAY",2);
  6. }

执行会报错:

  1. Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
  2. at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
  3. at person.andy.concurrency.eu.Day.main(Day.java:13)

我们去看Constructor.newInstance方法,会看到这样的判断:

  1. if ((clazz.getModifiers() & Modifier.ENUM) != 0)
  2. throw new IllegalArgumentException("Cannot reflectively create enum objects");

所以,通过反射来构造枚举实例的行为也是被禁止的。
枚举类的特点总结:
(1)枚举实例必须在enum关键字声明的类中显式的指定(首行开始的以第一个分号结束)。
(2)除了1,没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例。
(3)枚举类不可被继承。
(4)枚举类是线程安全的。
(5)枚举类型是类型安全的。
(6)无法继承其他类(已经默认继承Enum)。

带有额外成员变量的情况

我们上面看到,当声明一个类似于Day这样的枚举类时,编译器会为其自动生成一个私有构造方法,该构造方法的参数是name和ordinal,并且是通过调用父类Enum的构造方法来实现的。

  1. private Day(String s, int i)
  2. {
  3. super(s, i);
  4. }

如果我们声明的枚举有额外的成员变量呢,类似下面的示例:

  1. public enum Day {
  2. SUNDAY("sunday"),MONDAY("monday");
  3. private String cost;
  4. Day(String cost) {
  5. this.cost = cost;
  6. }
  7. }

我们增加了cost成员变量,并且在一个构造方法中使用了这个成员变量。注意这个构造方法没有任何修饰符,编译器会自动为它加上private限制。
还是直接查看jad反编译的代码:

  1. package person.andy.concurrency.eu;
  2. public final class Day extends Enum
  3. {
  4. public static Day[] values()
  5. {
  6. return (Day[])$VALUES.clone();
  7. }
  8. public static Day valueOf(String name)
  9. {
  10. return (Day)Enum.valueOf(person/andy/concurrency/eu/Day, name);
  11. }
  12. private Day(String s, int i, String cost)
  13. {
  14. super(s, i);
  15. this.cost = cost;
  16. }
  17. public static final Day SUNDAY;
  18. public static final Day MONDAY;
  19. private String cost;
  20. private static final Day $VALUES[];
  21. static
  22. {
  23. SUNDAY = new Day("SUNDAY", 0, "sunday");
  24. MONDAY = new Day("MONDAY", 1, "monday");
  25. $VALUES = (new Day[] {
  26. SUNDAY, MONDAY
  27. });
  28. }
  29. }

可以发现,编译器自动生成了新的构造方法,该构造方法有3个参数,其中一个是cost,而另外两个就是name和ordinal,这两个的初始化是通过Enum的构造方法来实现的。并且这个构造方法是私有的。

关于枚举类的Finalize方法

Enum类中重写了finalize方法,但是是个空实现,并且不能被子类继承,也就是说枚举类不能有finalize方法,这样做是为了保证枚举类实例在jvm中是一直存在的。

  1. /**
  2. * enum classes cannot have finalize methods.
  3. */
  4. protected final void finalize() { }

小结

枚举作为语法糖的一种,比自动装箱、拆箱要复杂一点,但是相比如泛型,就简单很多了。我们直接查看编译器生成的文件就能理解很多东西。有了上面对枚举的理解,我们就能知道在哪些情况下使用枚举,也更能知道自己在创建一个枚举时发生了什么。