JDK 1.5 开始提供了 enum 关键字,用来定义枚举类。Java 中的枚举类,是一组预定义常量的集合,本质上是一种多例设计。
* 本文系OOP的一次作业,欢迎朋友们批评指正。
以下面的Direction枚举类为例:
enum Direction {
EAST, SOUTH, WEST, NORTH
}
反编译其字节码可以发现,使用 enum 定义的类继承自 Enum 抽象类;由 final 修饰,所以无法被继承。
final class cn.basic.Direction extends java.lang.Enum<cn.basic.Direction>
minor version: 0
major version: 61
flags: (0x4030) ACC_FINAL, ACC_SUPER, ACC_ENUM
this_class: #1 // cn/basic/Direction
super_class: #26 // java/lang/Enum
枚举类继承了 Enum 中 name 和 ordinal 属性,以及一些相关方法。
关于 Enum,可以参见:
类的内部,初始化四个枚举常量为静态对象,并且创建了包含这四个对象的对象数组 $VALUES;构造函数私有,所以无法在类外实例化;重写了 Enum 的 values 和 valueOf 方法。
{
public static final cn.basic.Direction EAST;
descriptor: Lcn/basic/Direction;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final cn.basic.Direction SOUTH;
descriptor: Lcn/basic/Direction;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final cn.basic.Direction WEST;
descriptor: Lcn/basic/Direction;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final cn.basic.Direction NORTH;
descriptor: Lcn/basic/Direction;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static cn.basic.Direction[] values();
descriptor: ()[Lcn/basic/Direction;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #16 // Field $VALUES:[Lcn/basic/Direction;
3: invokevirtual #20 // Method "[Lcn/basic/Direction;".clone:()Ljava/lang/Object;
6: checkcast #21 // class "[Lcn/basic/Direction;"
9: areturn
LineNumberTable:
line 31: 0
public static cn.basic.Direction valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)Lcn/basic/Direction;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class cn/basic/Direction
2: aload_0
3: invokestatic #25 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class cn/basic/Direction
9: areturn
LineNumberTable:
line 31: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 name Ljava/lang/String;
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: new #1 // class cn/basic/Direction
3: dup
4: ldc #35 // String EAST
6: iconst_0
7: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #3 // Field EAST:Lcn/basic/Direction;
13: new #1 // class cn/basic/Direction
16: dup
17: ldc #37 // String SOUTH
19: iconst_1
20: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #7 // Field SOUTH:Lcn/basic/Direction;
26: new #1 // class cn/basic/Direction
29: dup
30: ldc #38 // String WEST
32: iconst_2
33: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #10 // Field WEST:Lcn/basic/Direction;
39: new #1 // class cn/basic/Direction
42: dup
43: ldc #39 // String NORTH
45: iconst_3
46: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
49: putstatic #13 // Field NORTH:Lcn/basic/Direction;
52: invokestatic #40 // Method $values:()[Lcn/basic/Direction;
55: putstatic #16 // Field $VALUES:[Lcn/basic/Direction;
58: return
LineNumberTable:
line 32: 0
line 31: 52
}
具体实现上,在类初始化 clinit 中,就是new了四个方向的对象,分别以其名字和0、1、2、3的序号为参数调用其(实际上是父类 Enum 的)构造函数,然后放到了对应的静态成员中。之后把它们放到了对象数组中。
下面,我编写了一个类,来模拟实现枚举类的一些功能。
final class Direction {
private final String name;
private final int ordinal;
public static final Direction EAST = new Direction("EAST", 0);
public static final Direction SOUTH = new Direction("SOUTH", 1);
public static final Direction WEST = new Direction("WEST", 2);
public static final Direction NORTH = new Direction("NORTH", 3);
private static final Direction[] $VALUES = new Direction[]{EAST, SOUTH, WEST, NORTH};
private Direction(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
@Override
public final String toString() {
return name;
}
public static Direction[] values() {
return $VALUES.clone();
}
// public static Direction valueOf(String str) {
// }
public final int compareTo(Direction d) {
return this.ordinal - d.ordinal;
}
}
注:未实现 valueOf 方法。
测试其功能如下:
然而,正如注释掉的那四行代码所揭示的,自定义类无法像原本的 enum Direction 一样自如地在 switch case 中做判断,因为编译器根本无法解析符号 'EAST' 和 'WEST'。
那么,Java 是如何实现 switch 语句中枚举项的判断的?
我们通过如下测试类探究一二:
public class Demo06 {
public static void main(String[] args) {
Direction dir = Direction.EAST;
/*if (dir == Direction.EAST) {
System.out.println("东");
}
System.out.println(dir.name());
System.out.println(dir.ordinal());
for (Direction direction : Direction.values()) {
System.out.println(direction);
}
System.out.println(dir.compareTo(Direction.WEST));
System.out.println(Direction.valueOf("WEST").ordinal());*/
switch (dir) {
case EAST -> System.out.println("E");
case WEST -> System.out.println("W");
}
}
}
enum Direction {
EAST, SOUTH, WEST, NORTH
}
不难看出,对于枚举类的各种方法,底层无非都是 getstatic 获取枚举常量后 invokevirtual 执行方法,或是直接 invokestatic 通过类名调用静态方法。
那 switch 语句呢?
我们惊奇地发现,这里多了一个叫 Demo06$1 的静态匿名内部类。它是编译器自动生成的,找到并反编译其字节码:
class cn.basic.Demo06$1
minor version: 0
major version: 61
flags: (0x1020) ACC_SUPER, ACC_SYNTHETIC
this_class: #8 // cn/basic/Demo06$1
super_class: #26 // java/lang/Object
interfaces: 0, fields: 1, methods: 1, attributes: 4
Constant pool:
#1 = Methodref #2.#3 // cn/basic/Direction.values:()[Lcn/basic/Direction;
#2 = Class #4 // cn/basic/Direction
#3 = NameAndType #5:#6 // values:()[Lcn/basic/Direction;
#4 = Utf8 cn/basic/Direction
#5 = Utf8 values
#6 = Utf8 ()[Lcn/basic/Direction;
#7 = Fieldref #8.#9 // cn/basic/Demo06$1.$SwitchMap$cn$basic$Direction:[I
#8 = Class #10 // cn/basic/Demo06$1
#9 = NameAndType #11:#12 // $SwitchMap$cn$basic$Direction:[I
#10 = Utf8 cn/basic/Demo06$1
#11 = Utf8 $SwitchMap$cn$basic$Direction
#12 = Utf8 [I
#13 = Fieldref #2.#14 // cn/basic/Direction.EAST:Lcn/basic/Direction;
#14 = NameAndType #15:#16 // EAST:Lcn/basic/Direction;
#15 = Utf8 EAST
#16 = Utf8 Lcn/basic/Direction;
#17 = Methodref #2.#18 // cn/basic/Direction.ordinal:()I
#18 = NameAndType #19:#20 // ordinal:()I
#19 = Utf8 ordinal
#20 = Utf8 ()I
#21 = Class #22 // java/lang/NoSuchFieldError
#22 = Utf8 java/lang/NoSuchFieldError
#23 = Fieldref #2.#24 // cn/basic/Direction.WEST:Lcn/basic/Direction;
#24 = NameAndType #25:#16 // WEST:Lcn/basic/Direction;
#25 = Utf8 WEST
#26 = Class #27 // java/lang/Object
#27 = Utf8 java/lang/Object
#28 = Utf8 <clinit>
#29 = Utf8 ()V
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 LocalVariableTable
#33 = Utf8 StackMapTable
#34 = Utf8 SourceFile
#35 = Utf8 Demo06.java
#36 = Utf8 EnclosingMethod
#37 = Class #38 // cn/basic/Demo06
#38 = Utf8 cn/basic/Demo06
#39 = Utf8 NestHost
#40 = Utf8 InnerClasses
{
static final int[] $SwitchMap$cn$basic$Direction;
descriptor: [I
flags: (0x1018) ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: invokestatic #1 // Method cn/basic/Direction.values:()[Lcn/basic/Direction;
3: arraylength
4: newarray int
6: putstatic #7 // Field $SwitchMap$cn$basic$Direction:[I
9: getstatic #7 // Field $SwitchMap$cn$basic$Direction:[I
12: getstatic #13 // Field cn/basic/Direction.EAST:Lcn/basic/Direction;
15: invokevirtual #17 // Method cn/basic/Direction.ordinal:()I
18: iconst_1
19: iastore
20: goto 24
23: astore_0
24: getstatic #7 // Field $SwitchMap$cn$basic$Direction:[I
27: getstatic #23 // Field cn/basic/Direction.WEST:Lcn/basic/Direction;
30: invokevirtual #17 // Method cn/basic/Direction.ordinal:()I
33: iconst_2
34: iastore
35: goto 39
38: astore_0
39: return
Exception table:
from to target type
9 20 23 Class java/lang/NoSuchFieldError
24 35 38 Class java/lang/NoSuchFieldError
LineNumberTable:
line 22: 0
LocalVariableTable:
Start Length Slot Name Signature
StackMapTable: number_of_entries = 4
frame_type = 87 /* same_locals_1_stack_item */
stack = [ class java/lang/NoSuchFieldError ]
frame_type = 0 /* same */
frame_type = 77 /* same_locals_1_stack_item */
stack = [ class java/lang/NoSuchFieldError ]
frame_type = 0 /* same */
}
SourceFile: "Demo06.java"
EnclosingMethod: #37.#0 // cn.basic.Demo06
NestHost: class cn/basic/Demo06
InnerClasses:
static #8; // class cn/basic/Demo06$1
一览无余,在合成的内部类中有一个静态成员 int 数组 $SwitchMap$cn$basic$Direction,其长度为枚举常量的个数,然后以枚举常量的 ordinal 为索引为数组元素赋值1、2、3……。这实际上实现了一个个枚举常量到整数的映射。
也就是说,对 Direction dir 的 switch 实际上转换成了对 Demo06$1.$SwitchMap$cn$basic$Direction[dir.ordinal()] 的判断。在 main 函数中,Java 就反向操作,获取要判断的枚举常量的 ordinal,对应到数组中的元素,之后再 lookupswitch 就行得通了。
在字节码层面,可以认为 switch 只支持 int 类型。而 Java 通过这种迂回的方式,扩展到了对枚举类的支持。That explains it.