深入理解枚举
最近刚学习完JVM相关知识,想到枚举既然这么异类,那就从字节码角度来分析一下它。有关枚举的讲解,很多博客已经很详细了,这里我们就从字节码的角度重新来认识一下它。
枚举类是一种特殊的类,它可以有自己的成员变量,方法,可以实现一个或多个接口,也可也定义自己的构造器。
1. 枚举类的继承结构:
2. 枚举类和普通类的区别:
(1)枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类,因此枚举不能够显示继承其他父类(单继承局限性,但是可以实现接口)。其中“java.lang.Enum”实现了“Comparable”和“Serializable”接口。
(2)使用enum定义,非抽象的枚举类默认会使用final修饰,因此枚举类不能够派生子类。
(3)枚举类的构造器只能够使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
(4)枚举类的所有实例必须要在枚举类的第一行显示列出,否则这个枚举类永远都能产生实例。列出这些实时,系统会自动添加public static final修饰符,无需程序员显示添加。
(5)枚举类默认提供了一个values方法,返回枚举实例数组,该方法可以很方便的遍历所有的枚举值。
为了能够更好的说明,上面的这些不同之处,下面我们定义了一个枚举类,使用“javap -v -p ”来反编译它
enum Season {
SPRING("春"),SUMMER("夏"),AUTUMN("秋"),WINTTER("冬");
private String name;
Season(String name) {
this.name = name;
}
}
反编译可以得到这些信息:
$ javap -v -p Season.class
Classfile /D:/Project/JvmDemo/target/classes/com/bigdata/java/Season.class
Last modified 2020-3-21; size 1206 bytes
MD5 checksum e78087beee7e634071bc6cd1e019c168
Compiled from "Demo69.java"
final class com.bigdata.java.Season extends java.lang.Enum
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
...
{
//枚举类中定义的常量,在字节码层面上的反映
public static final com.bigdata.java.Season SPRING;
descriptor: Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.bigdata.java.Season SUMMER;
descriptor: Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.bigdata.java.Season AUTUMN;
descriptor: Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.bigdata.java.Season WINTTER;
descriptor: Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
//枚举数组,这个数组供values方法使用,用于返回枚对象数组
private static final com.bigdata.java.Season[] $VALUES;
descriptor: [Lcom/bigdata/java/Season;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
//自动生成了values方法,它所完成的功能就是$VALUES.clone
public static com.bigdata.java.Season[] values();
descriptor: ()[Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
//从类中获取静态字段$VALUES,然后压入到操作数栈的栈顶
0: getstatic #1 // Field $VALUES:[Lcom/bigdata/java/Season;
//调用实例方法clone,即出栈$VALUES,然后执行$VALUES.clone,由于clone是native方法并且有返回值,所以返回值会被压入到操作数的栈顶,此时操作数的栈顶是枚举对象数组
3: invokevirtual #2 // Method "[Lcom/bigdata/java/Season;".clone:()Ljava/lang/Object;
//弹出栈顶的枚举对象数组,检查它的类型是否符合给定的类型,检查完毕后再次压入到操作数栈的栈顶
6: checkcast #3 // class "[Lcom/bigdata/java/Season;"
//将操作数栈顶的值弹出,并返回到给调用处,这样返回的就是一个clone后的枚举对象数组
9: areturn
LineNumberTable:
line 9: 0
//它所完成的功能就是根据传入的字符串,调用父类的valueOf方法返回对应的枚举常量
public static com.bigdata.java.Season valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)Lcom/bigdata/java/Season;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #4 // class com/bigdata/java/Season
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/bigdata/java/Season
9: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 name Ljava/lang/String;
//枚举构造方法,接收String和int,String,第一个String是枚举常量字符串,第二个是枚举常量所定义的位置,第三个枚举中构造方法传入的值
private com.bigdata.java.Season(java.lang.String);
descriptor: (Ljava/lang/String;ILjava/lang/String;)V
flags: ACC_PRIVATE
Code:
stack=3, locals=4, args_size=4
//将本地变量表索引0上的引用类型元素压入到操作数的栈顶
0: aload_0
//将本地变量表索引1上的引用类型元素压入到操作数的栈顶。尽管此时本地变量表对应索引上,元素为空,但运行时局部变量表被被填充为枚举常量字符串
1: aload_1
//本地变量表索引2上的int型元素压入到操作数的栈顶,运行时本地变量表对应索引上会被填充为枚举常量定义的位置
2: iload_2
//从操作数栈中依次弹出说压入的值,然后调用父类的构造方法,Enum."":(Ljava/lang/String;I)V
3: invokespecial #6 // Method java/lang/Enum."":(Ljava/lang/String;I)V
//将本地变量表索引0上的引用类型元素压入到操作数的栈顶
6: aload_0
//将本地变量表索引3上的引用类型元素压入到操作数的栈顶
7: aload_3
//为对象中的字段设置值,从操作数栈中依次弹出枚举常量和构造方法所传入的值,为name字段赋值,如:SEASON.SPRING.name=“春”
8: putfield #7