日常编码中,我们常常用到 switch-case 语句
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 1:
result = 20;
break;
case 2:
result = 30;
break;
default:
result = 40;
}
return result;
}
javap 基本格式如下 : javap [options]
加上选项-p,可以显示private方法和字段。默认情况下,javap会显示访问权限为public、protected和默认级别的方法
加上选项-s,可以输出类型描述符签名信息
加上选项-c,可以对类文件进行反编译,可以显示出方法内的字节码
加上选项-v,可以显示更加详细的内容,比如版本号、类访问权限、常量池相关的信息
加上选项-l,可以用来显示行号表和局部变量表,有时不显示局部变量表
如果一定要看局部变量表,需要在javac编译的时候加-g选项,生成所有的调试信息选项,然后再使用javap -l就可以得到局部变量表了
输出
C:\>javac Test.java
C:\>javap -c -p Test.class
Compiled from "Test.java"
public class com.yxzapp.Test {
public com.yxzapp.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int switchInt(int);
Code:
0: iload_1 // 使用iload加载局部变量第1个变量压入操作数栈
1: tableswitch { // 0 to 2 // 从栈顶中弹出元素,检查是否在[0,2]之内
0: 28 // 如果为0,则程序计数器跳转到第28行
1: 34 // 如果为1,则程序计数器跳转到第34行
2: 40 // 如果为2,则程序计数器跳转到第40行
default: 46 // 如果不在[0,2]内,则程序计数器跳转到第46行
}
28: bipush 10 // 将常量10压入栈顶
30: istore_2 // 将栈顶元素存储到局部变量下标为2位置
31: goto 49
34: bipush 20
36: istore_2
37: goto 49
40: bipush 30
42: istore_2
43: goto 49
46: bipush 40
48: istore_2
49: iload_2 // 将局部变量表中下标2元素压入栈中
50: ireturn // 弹出栈顶元素,方法返回
}
public class Test {
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 1:
result = 20;
break;
case 4:
result = 30;
break;
default:
result = 40;
}
return result;
}
}
字节码
C:\>javac Test.java
C:\>javap -c -p Test.class
Compiled from "Test.java"
public class com.yxzapp.Test {
public com.yxzapp.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int switchInt(int);
Code:
0: iload_1 // 使用iload加载局部变量第1个变量压入操作
1: tableswitch { // 0 to 4 // 从栈顶中弹出元素,检查是否在[0,4]之内
0: 36
1: 42
2: 54
3: 54
4: 48
default: 54
}
36: bipush 10
38: istore_2
39: goto 57
42: bipush 20
44: istore_2
45: goto 57
48: bipush 30
50: istore_2
51: goto 57
54: bipush 40
56: istore_2
57: iload_2
58: ireturn
}
细心的同学会发现,代码中的 case 并没有出现 3 , 但是字节码出现了。 原因是编译器会对 case 的值做分析, 如果 case 的值比较 ”紧凑“ ,中间有少量断层或者没有断层, 采用 tableswitch 来实现 witch-case ; 如果 case 值有大量断层,会生成一些虚假的 case 帮忙补齐 , 这样可以实现 O(1)时间复杂度查找 。case 值已经被补齐为连续的值,通过下标就可以一次找到。
public class Test {
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 2:
result = 20;
break;
case 5:
result = 30;
break;
default:
result = 40;
}
return result;
}
}
字节码
C:\>javac Test.java
C:\>javap -c -p Test.class
Compiled from "Test.java"
public class com.yxzapp.Test {
public com.yxzapp.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int switchInt(int);
Code:
0: iload_1
1: lookupswitch { // 3
0: 36
2: 42
5: 48
default: 54
}
36: bipush 10
38: istore_2
39: goto 57
42: bipush 20
44: istore_2
45: goto 57
48: bipush 30
50: istore_2
51: goto 57
54: bipush 40
56: istore_2
57: iload_2
58: ireturn
}
如果还是采用前面的 tableswitch 补齐的方式 ,跳跃大就会生成上百个假 case 项 , calss 文件会爆炸式的增长,这种做法明显不合理。为了解决这个问题, 可以采用 lookupswitch 指令, 它的键值都是经过排序的,在查找上可以采用二分查找的方式, 时间复杂度为 O(log n)
switch-case 语句在case 比较稀疏的情况下,编译器会使用 lookupswitch 指令来实现 , 反之 , 编译器会使用 tableswitch 来实现
JVM 虚拟机规范中提到
Compilation of switch statements uses the tableswitch and lookupswitch instructions. The tableswitch instruction is used when the cases of the switch can be efficiently represented as indices into a table of target offsets. The default target of the switch is used if the value of the expression of the switch falls outside the range of valid indices.Where the cases of the switch are sparse, the table representation of the tableswitch instruction becomes inefficient in terms of space. The lookupswitch instruction may be used instead.
The Java virtual machine specifies that the table of the lookupswitch instruction must be sorted by key so that implementations may use searches more efficient than a linear scan. Even so, the lookupswitch instruction must search its keys for a match rather than simply perform a bounds check and index into a table like tableswitch. Thus, a tableswitch instruction is probably more efficient than a lookupswitch where space considerations permit a choice.
这一段大意是
switch语句的编译使用tableswitch和lookupswitch指令。当switch 的情况可以有效地表示为目标偏移表的索引时,使用 tableswitch 指令。如果switch 的表达式值超出有效索引的范围,则使用 switch 的默认目标。如果switch 的情况稀疏,tableswitch 的 table 表示在空间方面会变得低效。可以使用lookupswitch 指令。
Java虚拟机指定 lookupswitch 指令的表必须按键排序,以便实现可以使用比线性扫描更高效的搜索。即便如此,lookupswitch指令必须在其键中搜索匹配项,而不是简单地执行边界检查并索引到类似表的tableswitch中。因此,在空间考虑允许选择的情况下,tableswitch 指令可能比 lookupswitch 更有效率。
当执行tableswitch时,直接使用栈顶的 int 值作为表的索引来抓取跳转目的地并立即执行跳转。整个查找+跳转过程是一个O(1) 操作,这意味着它的速度非常快。
执行LookupSwitch时,会将堆栈顶部的 int 值与表中的键进行比较,直到找到匹配项,然后使用该键旁边的跳转目的地来执行跳转。由于查找切换表始终必须排序,以便对于每个 X < Y,keyX < keyY,因此整个查找+跳转过程是一个O(log n) 操作因为将使用二分搜索算法来搜索键(无需将 int 值与所有可能的键进行比较来查找匹配项或确定没有任何键匹配)。O(log n) 比 O(1) 慢一些,但仍然没问题,因为许多众所周知的算法都是 O(log n) 并且这些算法通常被认为很快;即使 O(n) 或 O(n * log n) 仍然被认为是一个相当好的算法(慢/坏算法有 O(n2)、O(n3) 甚至更糟)。