阅读完本文我相信大家会有不少收货的,如果遇到不懂的地方,请耐心查阅相关知识。
JDK7之后switch为什么可以支持String类型的条件判断
记得读大学教我们Java课程的老师曾说,switch判断条件的数据类型只支持int和char。但是现在看来,这句话就不是那么严谨了,因为JDK7之后,还支持String类型的判断条件。接下来分析一步步分析其中的原理。
示例代码
public class TestSwitch {
public static final java.lang.String CASE_ONE = "1";
public static final java.lang.String CASE_TWO = "2";
public static final java.lang.String CASE_THERE = "3";
public static final java.lang.String CASE_FOUR = "4";
public void testSwitch(String key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_THERE:
break;
case CASE_FOUR:
break;
default:
break;
}
}
}
接着通过以下命令,将该Java文件转成Class文件
javac TestSwtich.java
然后通过以下命令,将编译后的Class文件进行反编译
javap TesTSwitch.class
(注意:以上javac和javap命令是JDK工具提供的,如有不了解的可以通过javac -help和javap -help进行了解)
得到如下汇编代码。
public class TestSwitch {
public static final java.lang.String CASE_ONE;
public static final java.lang.String CASE_TWO;
public static final java.lang.String CASE_THERE;
public static final java.lang.String CASE_FOUR;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//这里开始是分析的重点
public void testSwitch(java.lang.String);
Code:
//将方法参数key加载进操作数栈
0: aload_1
//接着将该方法参数key存储到局部变量表
1: astore_2
//将int常量-1压入操作数栈中
2: iconst_m1
//接着将刚压入栈中的常量-1存储到局部变量表中
3: istore_3
//将局部变量表中的存储的方法参数key加载到操作数栈顶
4: aload_2
//这一步是关键,接着虚拟机会调用String的hashCode方法,
//即对示例源码中key值进行hashCode,这样做的目的就是转成对int类型的判断
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
//taleswitch是Java虚拟机对应Java源码中switch关键字的指令
8: tableswitch { // 49 to 52
//49对应着常量字符串“1”的hashCode值,也是字符‘1’的ASCII值,
//大家可以看下String类hasCode的源码就会知道为什么相等了。
//如果字符串key的hashCode值等于常量字符串“1”的hashCode值,
//则跳转到行号为40的地方继续执行指令
49: 40
50: 54
51: 68
52: 82
default: 93
}
//将局部变量表中的存储的方法参数key加载到操作数栈顶
40: aload_2
// 将常量池中的常量字符串“1”压入栈中
41: ldc #3
//接着调用String的equals方法将常量字符串“1”和key进行比较,接着讲返回值压入栈顶
//虽然equals方法的返回值是布尔类型,但是Java虚拟机会将布尔类型窄化成int型。
43: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
//从栈顶中弹出int型数据,如果为0则跳转到行号为93的地方进行执行,0代表false
46: ifeq 93
//将int型常量0压入栈中
//为什么会把0压入栈中?因为虚拟机会将第一个case情况默认赋值为0,后面的case情况依次+1
49: iconst_0
//将int型常量0存储到局部变量表中
50: istore_3
//接着跳转到行号为93的地方继续执行
51: goto 93
54: aload_2
55: ldc #5 // String 2
57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
60: ifeq 93
63: iconst_1
64: istore_3
65: goto 93
68: aload_2
69: ldc #6 // String 3
71: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
74: ifeq 93
77: iconst_2
78: istore_3
79: goto 93
82: aload_2
83: ldc #7 // String 4
85: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
88: ifeq 93
91: iconst_3
92: istore_3
//以上每种case情况,最终会跳转到该行指令执行
//上面分析中,如果case情况为字符串“1”,则会保存一个int常量0,
//这里0也就代表了case为“1”的情况。
//这句指令会把先前存储在局部变量表中的int值加载到栈顶
93: iload_3
94: tableswitch { // 0 to 3
//如果等于0,则跳转到124行指令处执行
0: 124
1: 127
2: 130
3: 133
default: 136
}
124: goto 136
127: goto 136
130: goto 136
133: goto 136
136: return
}
以上代码中,我分析了"case CASE_ONE:"情况,虽然我们Java代码中只用了一个switch关键字,但是编译器生成的字节码却用了两个tableswitch指令。第一条tableswitch指令是根据字符串key哈希之后的int值进行调整判断,跳转到相应的行号之后,接着调用equals方法进行字符串内容比较,如果内容相等,会将每种case情况用一个int值记录,从0开始依次加1。第二条tableswitch指令会根据每种case情况所对应的int值进行判断,最终转化为switch的判断条件为int类型的情况。
由此可见,用String类型作为判断条件,编译器编译后的指令也会相应的增加。因此建议,能够用int值作为判断条件的就用int值吧。
如果对Java虚拟机指令不了解的,请耐心翻阅相关书籍或查阅相关资料。我相信你也会有不少收货的。
如何写出高效的switch代码
看到这个标题,不要惊讶。我们边写示例边分析原理。
示例代码1
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_THERE = 3;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_THERE:
break;
default:
break;
}
}
}
反编译后得到:
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_THERE;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: tableswitch { // 1 to 3
1: 28
2: 31
3: 34
default: 37
}
28: goto 37
31: goto 37
34: goto 37
37: return
}
示例代码2
“示例代码2”在“示例代码1”的基础上,仅仅修改了最后一个case情况的判断常量数的值。从“3”变成“5"。
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_FIVE = 5;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_FIVE:
break;
default:
break;
}
}
}
反编译后得到
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_FIVE;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: tableswitch { // 1 to 5
//“示例代码2”相对“示例代码1”来说仅仅改变了最后一个case的判断常量数的值。
//但是会导致所有case判断常量数的值不连续了。“示例代码1”是“1,2,3”,这个三个数是连续的。
//但是本示例中变成了“1,2,5”,这个三个数就不连续了。tableswitch这个指令比较“聪明”的。
//如果判断数值是不连续的,且又不是那么离散,那么它会自动把中间缺的判断常量数给补上。
//例如下面代码,判断条件3和4是编译器帮我们补上的。
//补上后有什么好处呢?补上后,“1,2,3,4,5”这些判断条件值就是一个连续值的整型数组,利于判断的直接跳转。
//比如我们传入的判断条件数值是3,即switch(key)中key值为3,
//那么虚拟机会首先判断3是否在1-5之间,
//如果在则取目标值1(即下面“1:36”的这行代码)为参照,
//接着直接跳转到数组中(3-1)项(注意:数组是从0开始),即“3:45”代码出。
1: 36
2: 39
3: 45
4: 45
5: 42
default: 45
}
36: goto 45
39: goto 45
42: goto 45
45: return
}
示例代码3
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_TEN = 10;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_TEN:
break;
default:
break;
}
}
}
反编译后得到
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_TEN;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: lookupswitch { // 3
//这里我们将最后一个判断条件的数值改为10后,
//这里出现的不是tableswitch指令,而是lookupswitch指令了。
//原因是,case中所有的判断条件的数值比较离散了,如果系统继续帮我们补其剩余判断数的话(即从3到9),
//那么会浪费不少内存空间。因此这里改用lookupswitch指令,那么该指令是如何执行呢?
//其实很简单就是一步一步的比较下去,知道碰到条件满足的。
1: 36
2: 39
10: 42
default: 45
}
36: goto 45
39: goto 45
42: goto 45
45: return
}
这三种情况我只改变了最后一个case的判断常量数的值,但是得到的反编译代码却有所不同。
对于编译器来说,switch的case判断条件值,有三种情况。
1.判断值都是连续数字。
2.判断数字不连续但也不很离散。
3.判断数字比较离散。
以上第一种和第二种情况是使用tableswitch指令,第三种情况使用lookupswitch指令。
这里大家可能会对第二种情况有些许疑问,就是如何判断不连续但也不很离散,这里虚拟机有一套判断规则,会根据switch语句的case个数和case的所有判断常量数值的离散情况而定。
简而言之,tableswitch指令是以空间换时间来提供效率,而lookupswitch会“牺牲”效率来换取空间。但是我们不需要考虑这些,因为聪明的编译器会帮我们搞定。
使用switch关键字的建议。
switch是我们经常打交道的关键字,但是在写判断条件的时候我们不应该很随意设置。
比如
public class TestSwitch {
public static final int DO_SWIM = 1;
public static final int DO_EAT = 2;
public static final int DO_DRINK = 3;
……
public void testSwitch(int key) {
switch (key) {
case DO_SWIM:
break;
case DO_EAT:
break;
case DO_DRINK:
break;
……
default:
break;
}
}
}
DO_SWIM、DO_EAT、DO_DRINK……,我们分别代表三种不用行为,如果分别设置为1、2、3……这种连续数据。那么这种代码就是相当完美的。但是有时候有些程序员比较“另类”,可能会设成1,3,5,7……这种纯"奇"数据。或者10,100,1000,10000……这种霸气数据。后两种情况的数据要么是会造成空间浪费,要么是会影响执行效率。因此,我们在写switch指令时,如果情况允许,最好将case的判断数据设置为连续的,同时对减少apk体积也有那么点帮助。
写在最后
小小的代码背后都可能会蕴含高深的原理,保持一颗求知的心,我们才能不断进步,共勉之!当然,如果文章有何错误或写的不明白之处,欢迎大家指出,非常感谢阅读!