1.本文从字节码层面来探究,switch的case变量值为java枚举Enum时,编译器究竟做了什么工作。
2.源码如下
枚举类Color
public enum Color {
RED,BLUE,YELLOW
}
switch中使用Color
public class MySwitchTest {
int chooseColor(Color c){
switch (c){
case RED:
return 5;
case RED:
return 6;
case YELLOW:
return 7;
default:
return 8;
}
}
}
3.先看结果,javac编译后,MySwitchTest生成了一个内部类MySwitchTest$1,反编译后的MySwitchTest的代码大致如下:
public class MySwitchTest {
int chooseColor(Color c){
int a = MySwitchTest$1.$SwitchMap$Color[c.ordinal];
switch (a){
case 1:
return 5;
case 2:
return 6;
case 3:
return 7;
default:
return 8;
}
}
static class MySwitchTest$1 {
static final int[] $SwitchMap$Color;
static {
Color[] values = Color.values();
int length = values.length;
$SwitchMap$Color = new int[length];
try {
$SwitchMap$Color[Color.RED.ordinal()] = 1; //和switch中的case 1对应
} catch (NoSuchFieldError e) {
}
try {
$SwitchMap$Color[Color.BLUE.ordinal()] = 2; //和switch中的case 2对应
} catch (NoSuchFieldError e) {
}
try {
$SwitchMap$Color[Color.YELLOW.ordinal()] = 3; //和switch中的case 3对应
} catch (NoSuchFieldError e) {
}
}
}
}
4.我们来看MySwitchTest.java编译后生成的class文件,除了一个MySwitchTest.class外,还生成了一个MySwitchTest$1.class文件,MySwitchTest$1是MySwitchTest的一个内部类,是编译器javac在编译MySwitchTest.java时自动生成的。
运行 javap -v -p MySwitchTest\$1.class,可以得到内部类的字节码如下
Classfile /C:/workspace/test/target/classes/MySwitchTest$1.class
Last modified 2019-12-16; size 619 bytes
MD5 checksum 26074600ed9a7634433a37fd542bc348
Compiled from "MySwitchTest.java"
class MySwitchTest$1
minor version: 0
major version: 52
flags: ACC_SUPER, ACC_SYNTHETIC
Constant pool:
...
{
static final int[] $SwitchMap$Color;
descriptor: [I
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: invokestatic #1 // Method Color.values:()[LColor;
3: arraylength
4: newarray int
6: putstatic #2 // Field $SwitchMap$Color:[I
9: getstatic #2 // Field $SwitchMap$Color:[I
12: getstatic #3 // Field Color.RED:LColor;
15: invokevirtual #4 // Method Color.ordinal:()I
18: iconst_1
19: iastore
20: goto 24
23: astore_0
24: getstatic #2 // Field $SwitchMap$Color:[I
27: getstatic #6 // Field Color.BLUE:LColor;
30: invokevirtual #4 // Method Color.ordinal:()I
33: iconst_2
34: iastore
35: goto 39
38: astore_0
39: getstatic #2 // Field $SwitchMap$Color:[I
42: getstatic #7 // Field Color.YELLOW:LColor;
45: invokevirtual #4 // Method Color.ordinal:()I
48: iconst_3
49: iastore
50: goto 54
53: astore_0
54: return
}
SourceFile: "MySwitchTest.java"
EnclosingMethod: #22.#0 // MySwitchTest
InnerClasses:
static #8; //class MySwitchTest$1
字节码里省略了常量池的内容,以及静态块static{}的一些属性,我们从上依次分析一些重要的内容。
4.1
class MySwitchTest$1
minor version: 0
major version: 52
flags: ACC_SUPER, ACC_SYNTHETIC
major version: 52表示生成这段字节码的jdk版本是8。flags:acc_synthentic表示这个类是编译器自动生成的。
4.2
static final int[] $SwitchMap$Color;
descriptor: [I
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
从这一段很容易看出,MySwitchTest$1有一个字段int[] $SwitchMap$Color,flags:ACC_SYNTHETIC 表示该字段是编译器自动生成的。
4.3
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: invokestatic #1 // Method Color.values:()[LColor;
3: arraylength
4: newarray int
6: putstatic #2 // Field $SwitchMap$Color:[I
9: getstatic #2 // Field $SwitchMap$Color:[I
12: getstatic #3 // Field Color.RED:LColor;
15: invokevirtual #4 // Method Color.ordinal:()I
18: iconst_1
19: iastore
20: goto 24
23: astore_0
24: getstatic #2 // Field $SwitchMap$Color:[I
27: getstatic #6 // Field Color.BLUE:LColor;
30: invokevirtual #4 // Method Color.ordinal:()I
33: iconst_2
34: iastore
35: goto 39
38: astore_0
39: getstatic #2 // Field $SwitchMap$Color:[I
42: getstatic #7 // Field Color.YELLOW:LColor;
45: invokevirtual #4 // Method Color.ordinal:()I
48: iconst_3
49: iastore
50: goto 54
53: astore_0
54: return
MySwitchTest$1最重要的部分就是静态块static{},指令0~6,调用生成一个int型数组,数组长度和Color.values()的长度一致,并将数组赋值给$SwitchMap$Color字段。指令9~20,调用第一个case的值RED的ordinal(),返回值0,以0为下标,找到数组$SwitchMap$Color的下标为0的元素,赋值为1。指令23,异常处理代码,暂时忽略,后面专门讲解。指令24~35,逻辑与指令9~20相同,为数组$SwitchMap$Color下标为1的元素,赋值为2。指令38,异常处理代码,暂时忽略,后面专门讲解。指令39~50,逻辑与指令9~20相同,为数组$SwitchMap$Color下标为2的元素,赋值为3。指令53,异常处理代码,暂时忽略,后面专门讲解。指令54,return,方法结束。至此,MySwitchTest$1的值为{1, 2, 3}
5.了解了MySwitchTest$1的字节码逻辑之后,我们来看MySwitchTest的chooseColor()方法的字节码。
public int chooseColor(Color);
descriptor: (LColor;)I
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field MySwitchTest$1.$SwitchMap$Color:[I
3: aload_1
4: invokevirtual #3 // Method Color.ordinal:()I
7: iaload
8: tableswitch { // 1 to 3
1: 36
2: 38
3: 41
default: 44
}
36: iconst_5
37: ireturn
38: bipush 6
40: ireturn
41: bipush 7
43: ireturn
44: bipush 8
46: ireturn
指令0~7,获取MySwitchTest$1的字段,由上一节的分析可知该字段值为{1, 2, 3},调用参数Color的ordinal()方法,并以ordinal()方法的返回值为下标,获取MySwitchTest$1.$SwitchMap$Color的元素。由此可知,当参数为RED时,获取的元素为1,当参数BLUE时,获取到的元素为2,当参数YELLOW时,获取到的元素为3,这与上一节根据RED、BLUE、YELLOW元素生成MySwitchTest$1.$SwitchMap$Color的值的逻辑相同。指令8,根据switch生成的指令tableswitch,但是仔细观察case的值并不是RED,BLUE,YELLOW了,而是变成了1,2,3,这也和上一节根据RED、BLUE、YELLOW元素生成MySwitchTest$1.$SwitchMap$Color的值的逻辑相同。指令36~46,根据不同的case值返回不同的结果。
6.总结:由第4节的分析和第5节的分析,可知当switch的变量为Enum时,编译器会生成一个内部类并在该内部类存储一个整型数组,在数组的下标为Enum.ordinal()的位置处存放元素Enum.ordinal()+1。在switch匹配时,也会以枚举变量的ordinal()值为下表,从内部类数组中获取元素作为匹配值,而且switch的case值也是内部类数组值相同。另外,如果想了解switch的字节码,可以参考字节码层面理解Switch,如果想了解Enum的字节码,可以参考字节码层面理解java枚举Enum。