前言
本文从枚举的字节码方面看枚举,并学习下 swtich的实现 和 swtich(String) 的实现。
一、一个简单的枚举
public class EnumTest {
enum EnumDay {
MONDAY,
TUESDAY,
WENSDAY
}
public static void testEnum(EnumDay day){
switch (day){
case MONDAY:
break;
case TUESDAY:
break;
case WENSDAY:
break;
default:
break;
}
}
}
我们定义了一个枚举类EnumDay, 定义了三个值 MONDAY,TUESDAY,WENSDAY
接下来我们通过 javac EnumTest 看下编译后生成了 EnumTest.class EnumTestEnumDay.class 三个class 文件。
我们看下 EnumTest 的字节码文件(javap -c EnumTest)
{
public javaplan.EnumTest(); // 无参的构造方法
descriptor: ()V // 构造方法描述符,入参没有,返回值是 V 代表void
flags: ACC_PUBLIC // 方法访问标记 public
//操作数栈深度 1 局部变量表大小 1 参数个数 1 这里指this
Code:
stack=1, locals=1, args_size=1
0: aload_0 //将局部变量表第0 个位置加载到操作数栈顶,即 this
1: invokespecial #1 // Method java/lang/Object."<init>":()V //用this 执行init 方法
4: return
LineNumberTable:
line 3: 0
//TestEnum 方法实现
public static void testEnum(javaplan.EnumTest$EnumDay);
descriptor: (Ljavaplan/EnumTest$EnumDay;)V //方法描述符:入参是EnumDay,返回值是V,表示无返回值
flags: ACC_PUBLIC, ACC_STATIC //方法描述符:public,static
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field javaplan/EnumTest$1.$SwitchMap$javaplan$EnumTest$EnumDay:[I
3: aload_0 //由于是static 方法,无this 指针作为默认参数,这里是把入参EnumDay 放入栈顶
4: invokevirtual #3 // Method javaplan/EnumTest$EnumDay.ordinal:()I 执行EnumDay.original 方法 放入局部变量表
7: iaload //将 上一步的值加载到栈顶
8: tableswitch { // 1 to 3
1: 36 //如果是 1 跳转至 36行
2: 39 //如果是 2 跳转至 39行
3: 42 //如果是 3 跳转至 42行
default: 45 //否则跳转至 45行
}
36: goto 45 //跳转至45行
39: goto 45 //跳转至45行
42: goto 45 //跳转至45行
45: return
//内部类 EnumTest$1 和 EnumTest$EnumDay
InnerClasses:
static #6; //class javaplan/EnumTest$1
static final #9= #8 of #4; //EnumDay=class javaplan/EnumTest$EnumDay of class javaplan/EnumTest
上述代码流程看到生成了 三个类,也就比平常多了一个EnumDay,通过执行 EnumDay.original 然后判断该跳转执行哪一行,似乎并不能看出性能不好在哪里,接下来我们再看一个例子。
二、枚举类
public enum SingleEnumTest {
YELLOW,
RED,
GREEN
}
依然我们编译成 字节码文件 再来看字节码:
public final class javaplan.SingleEnumTest extends java.lang.Enum<javaplan.SingleEnumTest> {
public static final javaplan.SingleEnumTest YELLOW;
public static final javaplan.SingleEnumTest RED;
public static final javaplan.SingleEnumTest GREEN;
public static javaplan.SingleEnumTest[] values();
Code:
0: getstatic #1 // Field $VALUES:[Ljavaplan/SingleEnumTest;
3: invokevirtual #2 // Method "[Ljavaplan/SingleEnumTest;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Ljavaplan/SingleEnumTest;"
9: areturn
public static javaplan.SingleEnumTest valueOf(java.lang.String);
Code:
0: ldc #4 // class javaplan/SingleEnumTest
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class javaplan/SingleEnumTest
9: areturn
静态代码块
static {};
Code:
//生成静态示例 SingleEnumTest YELLOW
0: new #4 // class javaplan/SingleEnumTest
3: dup
4: ldc #7 // String YELLOW
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field YELLOW:Ljavaplan/SingleEnumTest;
//生成静态示例 SingleEnumTest RED
13: new #4 // class javaplan/SingleEnumTest
16: dup
17: ldc #10 // String RED
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field RED:Ljavaplan/SingleEnumTest;
//生成静态示例 SingleEnumTest GREEN
26: new #4 // class javaplan/SingleEnumTest
29: dup
30: ldc #12 // String GREEN
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field GREEN:Ljavaplan/SingleEnumTest;
//生成一个大小为3的数组
39: iconst_3
40: anewarray #4 // class javaplan/SingleEnumTest
//YELLOW 对象存入数组第0 个位置
43: dup
44: iconst_0
45: getstatic #9 // Field YELLOW:Ljavaplan/SingleEnumTest;
48: aastore
//RED 对象存入数组的第1 个位置
49: dup
50: iconst_1
51: getstatic #11 // Field RED:Ljavaplan/SingleEnumTest;
54: aastore
//GREEN对象存入数组的第2个位置
55: dup
56: iconst_2
57: getstatic #13 // Field GREEN:Ljavaplan/SingleEnumTest;
60: aastore
//数组对象放入局部变量表
61: putstatic #1 // Field $VALUES:[Ljavaplan/SingleEnumTest;
64: return
}
可以看到 每个枚举值 都是一个对象,且会生成一个数组,来保存枚举对象 这样会很耗内存 。上面我们提到 了枚举一般作为配合 swtich 使用,那么switch 的实现 有什么性能瓶颈么?
三、switch 的底层实现
(1) tableswtich:
//java代码:
public void switch1(int i){
switch (i){
case 0:
break;
case 2:
break;
case 3:
break;
default:
break;
}
}
//对应的字节码:
public void switch1(int);
Code:
0: iload_1 //从局部变量表加载参数到栈顶即i
1: tableswitch { // 0 to 3
0: 32 //如果i= 0 跳转至32行
1: 41 //如果i= 1 跳转至41行
2: 35 //如果i= 2 跳转至35行
3: 38 //如果i= 3 跳转至38行
default: 41 //其他 跳转至41行
}
32: goto 41
35: goto 41
38: goto 41
41: return
可以看到逻辑非常简单,但是我们代码中并未写 case=1 的情况,生成的字节码指令默认给我们加上了case=1的情况,这样的话 那 如果 我中间隔100个 还会给我补全100个吗?接下来我们看下 switch 的另一中实现方式 lookupswtich
(2)lookupswitch
//java 代码
public void switch2(int i){
switch (i){
case 0:
break;
case 10:
break;
case 20:
break;
default:
break;
}
}
//对应的字节码
public void switch2(int);
Code:
0: iload_1 //从局部变量表加载参数到栈顶即i
1: lookupswitch { // 3
0: 36 //0 跳转至 36行
10: 39 // 10 跳转至39行
20: 42 // 20 跳转至 42行
default: 45 // 其他跳转至 45行
}
36: goto 45
39: goto 45
42: goto 45
45: return
逻辑也非常简单 ,由于跨度大,0 到 10 到20 底层采用了 lookupswtich 的实现,并没有补全 0 到10 和 10到20 中间的情况,可以看到 case 在不紧凑的情况下会采用 lookupswtich 来实现,lookup 将 case 值进行了排序,这样可以提高查找速率(lookup二分查找)感兴趣的朋友可以自己深入研究下。
(3)tableswitch 与lookupswtich 选择依据
很显然第一个依据是 case 是否紧凑,但这个只能作为大致的判断依据,准确的还要根据时间和空间两个维度来判断。
Tableswtich 性能计算:
tablespacecost = 4 + ((long) hi - lo + 1); hi 表示case 值的最大值 ,lo 表示case 值的最小值
tabletimecost = comparisons 比较次数 基本和 hi- lo +1 相等
LookUpSwtich 性能计算:
lookupspacecost = 3 + 2* tables; tables 表示 case 个数
lookuptimecost = tables
最终选择:
tablespacecost + 3*tabletimecost < lookupspacecost + 3 * lookuptimecost ? tableswtich : lookupswtich
可以看到,选择tableswtich或者 lookupswtich 会根据case 紧凑与否,并实际根据时间和空间消耗来选择
三、switch (String)的底层实现
JDK7 开始实现了 case 的值可以是 String,我们知道 enum 作为case 时是转为了int,那String 能和 int 产生关联的,我们是否想到了 hashcode,这只是我们的猜想,我们来看下具体的字节码。
//java 代码
public int stringSwitch(String name){
switch (name){
case "java":
return 1;
case "kotlin":
return 2;
default:
return 3;
}
}
//字节码
public int stringSwitch(java.lang.String);
Code:
0: aload_1 //将入参 String加载到操作数栈顶
1: astore_2 //将String 从操作数栈顶 放到局部变量表第二个位置
2: iconst_m1 //将-1 加载到操作数栈顶
3: istore_3 //将-1 从操作数栈顶 放到 局部变量表第三个位置
4: aload_2 //加载 局部变量表第二个位置即string 到栈顶
5: invokevirtual #2 // Method java/lang/String.hashCode:()I,执行String hashCode
8: lookupswitch { // 2
-1125574399: 50 //"kotlin" hashCode值跳转至 50行
3254818: 36 // “java” hashCode值跳转至 36行
default: 61 //其他跳转至 61行
}
36: aload_2 //hashcode 值是 3254818 跳转至36行,加载String 到栈顶
37: ldc #3 // String java 从常量池加载字符串 "java"
39: invokevirtual #4 // String.equals:(Ljava/lang/Object;)Z 判断两个字符串是否相等
42: ifeq 61 //如果不相等跳转至 61行
45: iconst_0 //将 0 加载到操作数栈顶
46: istore_3 //将 0 存入局部变量表第三个位置
47: goto 61 //跳转至 61行
// 50-60行 处理字符串是 kotlin 的情况 和上述 java 字符串流程一致
50: aload_2
51: ldc #5 // String kotlin
53: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 61
59: iconst_1
60: istore_3
61: iload_3 //加载局部变量表第三个位置到操作数栈顶,java 存入的是 0,kotlin 存入的是 1
62: lookupswitch { // 2
0: 88 //0 跳转至 88行
1: 90 //1 跳转至 90行
default: 92 //其他 跳转至 92行
}
88: iconst_1 //将1 加载到操作数栈顶
89: ireturn //返回 1
90: iconst_2 //将2 加载到操作数栈顶
91: ireturn //返回 2
92: iconst_3 //将3 加载到操作数栈顶
93: ireturn //返回3