Java 从JDK1.5开始引入了枚举特性,用户可以直接采用与C++类似的方式定义枚举类型。有趣的是,Java 枚举的实现是在字节码编译器层次,并未修改JVM底层。本文主要在字节码层面分析Java Enum的实现方式。代码1在enumtestpkg包中定义了MyColor枚举类型,其包含red,blue两个枚举常量。
#代码1
package enumtestpkg;
public enum MyColor {
red,blue
}
一. java.lang.Enum类介绍
要了解Java Enum实现方式,就不得不提java.lang包下的Enum类,其类定义如下:
#代码2
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
所有用户定义的枚举类型都继承自Enum类(稍后将会在字节码中看到隐式继承),它是一个抽象类,并且包含一个继承自该类的类作为泛型参数。一个Enum类对象对应一个枚举常量,Enum类中定义了两个私有常量字段,String name表示用户声明的枚举常量名称;int ordinal表示用户在定义枚举常量时的顺序,通常用户不会用到该字段,其设计是为了用于如EnumSet和EnumMap等基于枚举的复杂数据结构。上述两个字段具有对应的public方法供用户访问。
Enum中其它重要方法介绍如下(参考JavaDoc):
- protected Enum(String name, int ordinal) :唯一构造方法,供编译器使用,用户无法调用。
- public final boolean equals(Object other): 其内部直接使用“==”结果作为返回值,即对于Enum对象来说,其equals方法比较的是引用
- clone() : 调用此方法会抛出CloneNotSupportException异常,这能够保证枚举常量的单例状态
- public final Class<E> getDeclaringClass():获取对应枚举常量的枚举类型。需要强调:两个枚举常量类型相同当且仅当二者该方法返回值相同,不能使用getClass方法作为判断依据。因为枚举常量定义时可以重写枚举类的方法,则此枚举常量getClass方法返回值是内部类对象。 例子如代码3所示:
#代码3
public enum MyFruit {
appale,
orange{
void print() {
System.out.println("orange");
System.out.println(getClass());
System.out.println(getDeclaringClass());
}
};
void print(){
System.out.println("default");
System.out.println(getClass());
System.out.println(getDeclaringClass());
};
}
public class TestEnum {
public static void main(String[] args) {
for(MyFruit f:MyFruit.values()){
f.print();
System.out.println(f.getDeclaringClass());
}
}
}
输出结果如下:
default
class enumtestpkg.MyFruit
class enumtestpkg.MyFruit
orange
class enumtestpkg.MyFruit$1
class enumtestpkg.MyFruit
对于orange常量是MyFruit$1类的对象,该类继承自MyFruit类。getgetDeclaringClass方法会首先检查枚举类的父类是否为Enum类,如果是则返回当前枚举类对象,因此apple返回了MyFruit类。若父类不为Enum类,如orange,则返回其父类对象,因此orange也返回了MyFruit类。使用getDeclaringClass方法能够保证同一枚举类型的常量被判断为类型相同。
- compareTo(E o) :实现了Comparable接口,内部比较二者的ordinal值
- public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) :根据name的值返回对应的泛型常量。注意这个方法将会在用户的泛型类中被隐式调用。
- readObject、readObject:这两个方法在调用时均会抛出异常,这是为了防止使用这两个方法对枚举类型进行反序列化,这可能造成程序中枚举对象的单例唯一性。Java枚举类型序列化之后会保存各枚举常量的name字段,反序列化时可以使用valueOf(由编译器隐式生成)方法来获取枚举对象。
二.Java枚举实现原理分析
使用javap工具对代码1编译生成的MyColor.class文件进行反编译,结果如下:
#javap -c MyColor.class
public final class enumtestpkg.MyColor extends java.lang.Enum<enumtestpkg.MyColor> {
public static final enumtestpkg.MyColor red;
public static final enumtestpkg.MyColor blue;
static {};
Code:
0: new #1 // class enumtestpkg/MyColor
3: dup
4: ldc #13 // String red
6: iconst_0
7: invokespecial #14 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #18 // Field red:Lenumtestpkg/MyColor;
13: new #1 // class enumtestpkg/MyColor
16: dup
17: ldc #20 // String blue
19: iconst_1
20: invokespecial #14 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #21 // Field blue:Lenumtestpkg/MyColor;
26: iconst_2
27: anewarray #1 // class enumtestpkg/MyColor
30: dup
31: iconst_0
32: getstatic #18 // Field red:Lenumtestpkg/MyColor;
35: aastore
36: dup
37: iconst_1
38: getstatic #21 // Field blue:Lenumtestpkg/MyColor;
41: aastore
42: putstatic #23 // Field ENUM$VALUES:[Lenumtestpkg/MyColor;
45: return
public static enumtestpkg.MyColor[] values();
Code:
0: getstatic #23 // Field ENUM$VALUES:[Lenumtestpkg/MyColor;
3: dup
4: astore_0
5: iconst_0
6: aload_0
7: arraylength
8: dup
9: istore_1
10: anewarray #1 // class enumtestpkg/MyColor
13: dup
14: astore_2
15: iconst_0
16: iload_1
17: invokestatic #31 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
20: aload_2
21: areturn
public static enumtestpkg.MyColor valueOf(java.lang.String);
Code:
0: ldc #1 // class enumtestpkg/MyColor
2: aload_0
3: invokestatic #39 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class enumtestpkg/MyColor
9: areturn
}
从上述字节码中可以得到如下结论:
- 第1行:用户定义枚举类型后,将由编译器编译成字节码,生成“枚举类”。在字节码层面,Java使用类来实现枚举功能,这种设计不会影响Java虚拟机;
- 第2-3行:用户定义的枚举常量将被设定为枚举类的静态常量字段,所以用户在访问枚举常量的时候可以采用“类名.枚举常量名”进行访问。
- 第5-30行:生成一个静态块代码。其中,7-12,13-18行分别调用父类的构造函数(即一中提到的protected构造函数),依次创建了两个枚举对象,并赋值给red和blue,其ordial字段分别设置为0和1(iconst_0,iconst_1). 19-30行则新建了一个大小为2的静态枚举对象数组,并分别将上述red,blue字段添加到数组中。在第29行将该数组赋值给类内部隐式创建的VALUES静态字段。
- 第32-49行:隐式创建一个values()方法,该方法内部调用arraycopy方法放回3中创建的枚举类数组的一个复制对象引用。调用该方法可以获得该枚举类型的所有枚举类对象。
- 第51-58行:隐式创建一个valueOf(String)方法,该方法调用了一中所说的Enum类的ValueOf方法,返回名称为指定值的枚举类对象。
三.总结
1.Java 枚举在字节码层面使用已有的字节码实现并通过类进行包装;
2.Java枚举常量是单例模式,在反序列化时会被保护。这也是为何equal内部也是比较引用,因为枚举常量有很强的专一指向性。
3.一般意义上,用户定义枚举常量类型时可以实现普通类的功能,可以增加方法和属性。利用这个特点,可以实现类似于C++的在定义枚举常量时跳跃赋数值值(并非修改ordial的值,ordial字节码按严格顺序赋值,用户无法更改)。
参考资料:
1.Java Enum类源码及注释
2.Java Language Specifica
附:一个很不友好的枚举使用方式,猜一猜结果
MyFruit fruit=MyFruit.appale.orange;
System.out.println(fruit==MyFruit.appale);