日常编码中,我们常常用到switch语句,在我的另外一篇文章中【JAVA】优化if else的几种方式,也谈到了可以利用switch来优化if-else结构,那么switch底层究竟是如何实现的呢?
我们先写几个示例
第一个示例:case条件中的int值连续
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;
}
老规矩,反编译后得到:
(首先javac Main.java,之后javap -c -p Main.class)
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchInt(int);
Code:
0: iload_1 //将局部变量表下标为1的元素,也就是传入的i的值压入栈顶
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 //将栈顶元素10存入局部变量表的第3个位置上
31: goto 49 //跳转到底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 //将局部变量表中的第3个元素压入栈中
50: ireturn //弹出栈顶元素,方法返回
}
其中的tableswitch是后面会着重分析的内容,这里先放着。
这里有一个疑问:
为什么刚开始传进来的i的值,是存到局部变量表中的第二个位置,第一个位置到底放了啥?
因为该方法是一个实例方法,局部变量表的第一个位置总是存放着this引用。在构造方法中,已经使用aload_0将this应用存入到了局部变量表中的第一个位置上。
关于这些字节码指令,可以参考这篇文章jvm理论-字节码指令
第二个示例:case中的int值不连续
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 3:
result = 20;
break;
case 7:
result = 30;
break;
default:
result = 40;
}
return result;
}
现在改成0,3,7了,继续观察反编译后得到的内容:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchInt(int);
Code:
0: iload_1
1: lookupswitch { // 3
0: 36
3: 42
7: 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中的值,由连续改成了不连续,得到的字节码指令居然发生了变化,由tableswitch变成了lookupswitch。
看到这里,我们可以总结一下
tableswitch和lookupswitch两者的区别
tableswitch
tableswitch用来处理条件连续的情况。首先进行一次范围检查,检查不通过便直接返回default。检查通过后,会得到对应case语句的偏移量。
tableswitch使用数组结构存储偏移量,因此利用下标可以快速定位到偏移量,时间复杂度为O(N)。
注意:这里的“连续”可以理解为相对连续或半连续,012连续,013可以理解为半连续。013也会使用tableswitch,只不过里面缺少的2和default分支会有同样的偏移量。
lookupswitch
lookupswitch则用来处理条件不连续的情况,当条件大面积不连续时,tableswitch将会产生大量的额外空间。使用lookupswitch,会将case值进行排序,之后可以利用二分法快速查到对应的分支偏移量。
lookupswitch则是维护了一个经过key排序的(key,value)结构,查找复杂度一般为O(logN)。
第三个示例:使用String类型
在jdk1.7的时候,可以使用String类型作为case条件了。
public int switchString(String str) {
int result;
switch (str) {
case "a":
result = 10;
break;
case "c":
result = 20;
break;
case "f":
result = 30;
break;
default:
result = 40;
}
return result;
}
这次我们不直接看字节码指令,仅仅观察class文件:
public class Main {
public Main() {
}
public int switchString(String var1) {
byte var4 = -1;
switch(var1.hashCode()) {
case 97:
if (var1.equals("a")) {
var4 = 0;
}
break;
case 99:
if (var1.equals("c")) {
var4 = 1;
}
break;
case 102:
if (var1.equals("f")) {
var4 = 2;
}
}
byte var2;
switch(var4) {
case 0:
var2 = 10;
break;
case 1:
var2 = 20;
break;
case 2:
var2 = 30;
break;
default:
var2 = 40;
}
return var2;
}
}
可以看得出,主要流程为:
- 首先使用hashcode方法获取到字符串的哈希值
- 因为case条件中的字符串的哈希值大概率不连续,所以字节码指令一般是使用lookupswitch来保存对应的偏移量
- 之后执行偏移量的字节码指令,一上来会调用equals来判断var1是否与case条件中的String相匹配(因为可能存在两个不同字符串的哈希值相同的情况)
- 匹配成功后,会再利用tableswitch来查询var4代表的偏移量
- 最后返回var2
总的来说,使用String类型来作为case中的条件,本质上还是先转化为hashcode,接着查到对应的hashcode,最后再利用equals检测内容是否相同。
第四个示例:使用枚举类型
public enum Animal {
CAT, DOG
}
public int switchEnum(Animal animal) {
int result;
switch (animal) {
case CAT:
result = 1;
break;
case DOG:
result = 2;
break;
default:
result = 3;
}
return result;
}
编译该文件会得到3个文件,分别是
- Main.class 这个文件是肯定有的
- Main$Animal.class 这个是枚举类,也是正常的
- Main$1.class 这是个啥?
进到Main$1.class发现:
import com.yang.testSwitch.Main.Animal;
// $FF: synthetic class
class Main$1 {
static {
try {
$SwitchMap$com$yang$testSwitch$Main$Animal[Animal.CAT.ordinal()] = 1;
} catch (NoSuchFieldError var2) {
}
try {
$SwitchMap$com$yang$testSwitch$Main$Animal[Animal.DOG.ordinal()] = 2;
} catch (NoSuchFieldError var1) {
}
}
}
使用javap -p Main$1.class后:
class com.yang.testSwitch.Main$1 {
static final int[] $SwitchMap$com$yang$testSwitch$Main$Animal;
static {};
}
原来这个class里,声明了一个静态的数组,数组利用枚举的ordinal()值作为下标,数组中的元素依次递增。
那这个数组到底是干啥用的呢?
我们接着使用javap -c -p Main.class反编译得到:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchEnum(com.yang.testSwitch.Main$Animal);
Code:
0: getstatic #2 // Field com/yang/testSwitch/Main$1.$SwitchMap$com$yang$testSwitch$Main$Animal:[I
3: aload_1
4: invokevirtual #3 // Method com/yang/testSwitch/Main$Animal.ordinal:()I
7: iaload
8: lookupswitch { // 2
1: 36
2: 41
default: 46
}
36: iconst_1
37: istore_2
38: goto 48
41: iconst_2
42: istore_2
43: goto 48
46: iconst_3
47: istore_2
48: iload_2
49: ireturn
}
现在可以发现,首先是获取到这个静态数组,再调用枚举的ordinal()方法获取枚举的值,再将这个值当作静态数组的下标,获取这个静态数组中的某一个元素,然后从lookupswitch中寻找偏移量。
第五个示例:使用包装类型
public int switchByte(Byte i) {
int result;
switch (i) {
case 1:
result = 10;
break;
case 2:
result = 20;
break;
case 3:
result = 30;
break;
default:
result = 40;
}
return result;
}
反编译得到:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchByte(java.lang.Byte);
Code:
0: aload_1
1: invokevirtual #2 // Method java/lang/Byte.byteValue:()B
4: tableswitch { // 1 to 3
1: 32
2: 38
3: 44
default: 50
}
32: bipush 10
34: istore_2
35: goto 53
38: bipush 20
40: istore_2
41: goto 53
44: bipush 30
46: istore_2
47: goto 53
50: bipush 40
52: istore_2
53: iload_2
54: ireturn
}
可以很清晰的看到,调用了Byte.byteValue()方法完成了对Byte的拆箱工作,之后比较byte值即可。
总结
我们用一个表格,来直观的说明各种类型的底层操作:
类型 |
操作 |
基本类型 |
直接比较(条件半连续,采用tableswitch,否则lookupswitch) |
String |
首先获取hashcode,之后采用equals判断 |
Enum |
自动生成SwitchMap数组,下标是枚举的ordinal(),值是从1开始递增的整数 |
包装类型 |
先进行拆箱,之后比较基本类型 |
这里有几点需要注意的是:
(1)根据java虚拟机规范,java虚拟机的tableswitch和lookupswitch指令都只能支持int类型的条件值,所以switch不支持long、float、double与boolean,以及他们对应的包装类。
(2)break语句会在字节码层面生成一个goto语句,用来直接跳转出switch语句块。因此,如果有哪一次忘记写break,那么程序执行就会进入下一个case中,可能会造成某些匪夷所思的问题。