在Java中,If与Switch哪个效率更高?
在Java中,If与Switch哪个效率更高?
先说结论: 在Java中, 面对多条件判断的情况, Switch的效率比If更高. 但是If能进行更加复杂的条件判断, 使用场景比Switch更加丰富.
想要探究为什么Switch效率更高, 我们可以从Java编译后的字节码文件入手.
If-Else
若要对数字1到5进行判断, 输出对应的数字, 如果我们采用if-else实现, 代码如下:
public static void ifTest(int n) {
if (n == 1) {
System.out.println("n == 1");
} else if (n == 2) {
System.out.println("n == 2");
} else if (n == 3) {
System.out.println("n == 3");
} else if (n == 4) {
System.out.println("n == 4");
} else if (n == 5) {
System.out.println("n == 5");
} else {
System.out.println("default");
}
}
编译后的字节码如下:
0 iload_0
1 iconst_1
2 if_icmpne 16 (+14)
5 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
8 ldc #13 <n == 1>
10 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
13 goto 88 (+75)
16 iload_0
17 iconst_2
18 if_icmpne 32 (+14)
21 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
24 ldc #21 <n == 2>
26 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
29 goto 88 (+59)
32 iload_0
33 iconst_3
34 if_icmpne 48 (+14)
37 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
40 ldc #23 <n == 3>
42 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
45 goto 88 (+43)
48 iload_0
49 iconst_4
50 if_icmpne 64 (+14)
53 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
56 ldc #25 <n == 4>
58 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
61 goto 88 (+27)
64 iload_0
65 iconst_5
66 if_icmpne 80 (+14)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #27 <n == 5>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 88 (+11)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #29 <n > 10>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 return
完整的字节码太长, 但是if的五次判断除了判断的内容不一样, 其他都是基本一致的, 所以我们只截取分析第一次判断n是否为1的部分即可.
0 iload_0 // 将局部变量表中索引为0的变量(参数n)压入操作数栈
1 iconst_1 // 将常量1压入操作数栈
2 if_icmpne 16 (+14) // 将操作数栈顶的两者进行比较, 若n不等于1, 则跳转到字节码行号为16的地方继续执行(下一个if判断)
5 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;> // 获取System.out静态变量
8 ldc #13 <n == 1> // 从常量池中加载符号引用为#13的常量即字符串: n == 1
10 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V> // 调用System.out的println方法
13 goto 88 (+75) // 跳转到字节码行号为88的地方继续执行(return)
从上述对字节码的分析中我们可以发现, 在最差的情况下, 要将变量加载到操作数栈中五次, 并进行五次判断.
Switch
tableswitch指令
case顺序的情况
下面我们来看看Switch是怎么做的. 相应的Java代码如下:
public static void switchTest(int n) {
switch (n) {
case 1:
System.out.println("n == 1");
break;
case 2:
System.out.println("n == 2");
break;
case 3:
System.out.println("n == 3");
break;
case 4:
System.out.println("n == 4");
break;
case 5:
System.out.println("n == 5");
break;
default:
System.out.println("default");
break;
}
}
编译后的字节码如下:
0 iload_0
1 tableswitch 1 to 5
1: 36 (+35)
2: 47 (+46)
3: 58 (+57)
4: 69 (+68)
5: 80 (+79)
default: 91 (+90)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #13 <n == 1>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 99 (+55)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 99 (+44)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 99 (+33)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #25 <n == 4>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 99 (+22)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #27 <n == 5>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 goto 99 (+11)
91 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
94 ldc #29 <n > 10>
96 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
99 return
第一眼看上去, 似乎多了一条奇怪的指令tableswitch
. 实际上这条指令会为我们维护一个表结构. 在表中保存了switch case中的上下限. 对于在上下限范围内的case, 将与下限相减, 得到一个偏移量, 并将这个偏移量映射到相应的字节码行号上. 反应到字节码中, 就是第1行到第35行的位置.
1 tableswitch 1 to 5 // 这里的 1 to 5 即是表结构的上下限, 若输入的case并不在这个范围内(比如6), 则会直接跳转到default条件对应的字节码行号执行.
1: 36 (+35) // 某个case下, 需要跳转执行的字节码行号
2: 47 (+46)
3: 58 (+57)
4: 69 (+68)
5: 80 (+79)
default: 91 (+90)
看文字总是干巴巴的, 所以还是看图比较清晰.
通过对tableswitch
的说明, 可以发现, 使用switch只需要将局部变量n压入操作数栈一次, 并且也无需一个个去将n与case进行比较, 而是直接计算其在表中的偏移量就可以得知跳转的字节码行号. 因此, switch的速度要比if-else快很多.
case乱序的情况
要使用偏移量, 那么case必然是要顺序的. 若我们将case改成乱序, 按照52341的顺序, 那么还会使用tableswitch
吗? 答案是肯定的. 修改后的Java代码如下:
public static void switchTest(int n) {
switch (n) {
case 5:
System.out.println("n == 5");
break;
case 2:
System.out.println("n == 2");
break;
case 3:
System.out.println("n == 3");
break;
case 4:
System.out.println("n == 4");
break;
case 1:
System.out.println("n == 1");
break;
default:
System.out.println("default");
break;
}
}
生成的字节码如下:
0 iload_0
1 tableswitch 1 to 5
1: 80 (+79)
2: 47 (+46)
3: 58 (+57)
4: 69 (+68)
5: 36 (+35)
default: 91 (+90)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #27 <n == 5>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 99 (+55)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 99 (+44)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 99 (+33)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #25 <n == 4>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 99 (+22)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #13 <n == 1>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 goto 99 (+11)
91 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
94 ldc #29 <default>
96 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
99 return
这时候, 我们发现即使写代码的时候case是乱序的, 但是编译成字节码后, tableswitch
仍然是有序的. 只不过按照书写顺序, case 5的情况跳转的字节码行号最小, case 1的情况跳转的字节码行号最大.
case不连续的情况
上面的情况都是case连续的, 如果case并不连续, 例如: 1235, 阁下又该如何应对呢? Java代码如下:
public static void switchTest(int n) {
switch (n) {
case 1:
System.out.println("n == 1");
break;
case 2:
System.out.println("n == 2");
break;
case 3:
System.out.println("n == 3");
break;
//case 4:
// System.out.println("n == 4");
// break;
case 5:
System.out.println("n == 5");
break;
default:
System.out.println("default");
break;
}
}
编译所得字节码如下:
0 iload_0
1 tableswitch 1 to 5
1: 36 (+35)
2: 47 (+46)
3: 58 (+57)
4: 80 (+79)
5: 69 (+68)
default: 80 (+79)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #13 <n == 1>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 88 (+44)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 88 (+33)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 88 (+22)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #27 <n == 5>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 88 (+11)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #29 <default>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 return
我们可以只看其中的关键部分:
1 tableswitch 1 to 5
1: 36 (+35)
2: 47 (+46)
3: 58 (+57)
4: 80 (+79)
5: 69 (+68)
default: 80 (+79)
哦? 我们注释掉的case 4的情况, 在字节码中被补全了! 只是case 4跳转的字节码行号和case default一样, 与我们不写case 4是等价的. 这么做是为了保证在case不连续的情况下仍然可以通过偏移量快速得知跳转字节码行号.
lookupswitch指令
现在我们来考虑另一种新的情况. Java代码如下:
public static void switchTest(int n) {
switch(n) {
case 100:
System.out.println("n == 100");
break;
case 200:
System.out.println("n == 200");
break;
case 300:
System.out.println("n == 300");
break;
default:
System.out.println("default");
break;
}
}
这时候再来看编译后的字节码文件:
0 iload_0
1 lookupswitch 3
100: 36 (+35)
200: 47 (+46)
300: 58 (+57)
default: 69 (+68)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #31 <n == 100>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 77 (+33)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #33 <n == 200>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 77 (+22)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #35 <n == 300>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 77 (+11)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #29 <default>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 return
可以发现, 这时候使用的是lookupswitch指令而不是tableswitch了. 产生这种结果的原因也很容易理解. 在这个例子中, case的跨度很大, 如果和tableswitch指令一样将偏移量作为表中的索引, 那么将浪费大量空间来保存这个表(在这个例子中是从100到300共两百项).
实际上, 在JVM中使用二分查找来尽可能进行快速查找. 若case乱序, 首先需要将case进行排序(与使用tableswitch的情况一致). 随后将排序完成后的case放入表结构中. 本例具体查找流程见下图:
当Case是枚举类时
从JDK7开始, Java开始将Enum作为switch语句中的case标签. 但在The Java Virtual Machine Specification中却告诉我们, JVM中的tableswitch和lookupswitch指令只能支持int数据类型. 因此, 在处理枚举类时一定做了什么与处理int型数据不同的特殊操作. 测试Java代码如下:
public static void switchTest(Color c) {
switch (c) {
case RED:
System.out.println("RED");
break;
case BLUE:
System.out.println("BLUE");
break;
case YELLOW:
System.out.println("YELLOW");
break;
default:
System.out.println("default");
break;
}
}
enum Color {
RED, BLUE, YELLOW;
}
编译后的字节码如下:
0 getstatic #31 <org/example/chapter5/LookupSwitchAndTablesSwitch$1.$SwitchMap$org$example$chapter5$Color : [I>
3 aload_0
4 invokevirtual #37 <org/example/chapter5/Color.ordinal : ()I>
7 iaload
8 tableswitch 1 to 3
1: 36 (+28)
2: 47 (+39)
3: 58 (+50)
default: 69 (+61)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #43 <RED>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 77 (+33)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #45 <BLUE>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 77 (+22)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #47 <YELLOW>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 77 (+11)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #29 <default>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 return
实际上, 当使用Enum作为case标签时需要维护两个表格. 在第一个表格中, 索引为Enum对象的序号(Enum.ordinal()方法的返回值
), 而值为连续整数. 在第二张表格中, 与先前的tableswitch一样, 会保存第一张表格中的lo值和hi值, 并通过偏移量得到字节码行号. 本例查找流程如下图:
当Case是String字符串时
与Enum相同, 在JDK7中, 字符串也可以作为Case标签使用了. 相关Java代码如下:
public static void stringCaseTest(String s) {
switch (s) {
case "Aa":
System.out.println("Aa");
break;
case "aa":
System.out.println("aa");
break;
case "BB":
System.out.println("BB");
break;
default:
System.out.println("default");
}
}
编译后的字节码如下:
0 aload_0
1 astore_1
2 iconst_m1
3 istore_2
4 aload_1
5 invokevirtual #49 <java/lang/String.hashCode : ()I>
8 lookupswitch 2
2112: 36 (+28)
3104: 64 (+56)
default: 75 (+67)
36 aload_1
37 ldc #54 <BB>
39 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
42 ifeq 50 (+8)
45 iconst_2
46 istore_2
47 goto 75 (+28)
50 aload_1
51 ldc #60 <Aa>
53 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
56 ifeq 75 (+19)
59 iconst_0
60 istore_2
61 goto 75 (+14)
64 aload_1
65 ldc #62 <aa>
67 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
70 ifeq 75 (+5)
73 iconst_1
74 istore_2
75 iload_2
76 tableswitch 0 to 2
0: 104 (+28)
1: 115 (+39)
2: 126 (+50)
default: 137 (+61)
104 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
107 ldc #60 <Aa>
109 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
112 goto 145 (+33)
115 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
118 ldc #62 <aa>
120 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
123 goto 145 (+22)
126 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
129 ldc #54 <BB>
131 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
134 goto 145 (+11)
137 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
140 ldc #29 <default>
142 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
145 return
由于tableswitch和lookupswitch都只能处理int型数据的情况, 而要将String类型映射为int型数据, 很容易想到字符串的Hashcode. 实际上Java也是这么做的, 从第5行字节码中可以看到, 我们调用了s参数的hashCode方法. 由于字符串的Hash值并不是连续的, 所以采用了lookupswitch指令对Hash值进行二分查找. 但是我们知道, 仅仅比较字符串的Hash值相同是不足以判断两个字符串是否相等的(拥有相同Hash值的字符串不一定相等, 但拥有不同Hash值的字符串一定不同. 比如上述Java代码中的Aa
和BB
, 他们的Hash值就完全相同), 因此要进行标签精确的匹配, 我们就必须在通过hash值进行筛选后, 再对拥有相同Hash值的字符串进行equals比较. 比较完成后, 根据比较结果, 可以为一个局部变量赋连续的整数, 并将这个局部变量作为tableswitch指令查询的索引, 快速获得跳转的字节码行号. 上述过程从编译后的Class文件反编译回的java代码中也可以很明显地看到.
// 将编译后的Class文件进行反编译后得到的java代码
public static void stringCaseTest(String s) {
byte var2 = -1;
switch (s) {
case "BB":
var2 = 2;
if (s.equals("Aa")) {
var2 = 0;
}
break;
case "aa":
var2 = 1;
}
switch (var2) {
case 0:
System.out.println("Aa");
break;
case 1:
System.out.println("aa");
break;
case 2:
System.out.println("BB");
break;
default:
System.out.println("default");
}
}
本例具体查找流程见下图:
相关题目
最后再来几道与switch相关的题目.
public class SwitchTest {
public static void main(String[] args) {
//当default在中间时,且看输出是什么?
int a = 1;
switch (a) {
case 2:
System.out.println("print 2");
case 1:
System.out.println("print 1");
default:
System.out.println("first default print");
case 3:
System.out.println("print 3");
}
//当switch括号内的变量为String类型的外部参数时,且看输出是什么?
String param = null;
switch (param) {
case "param":
System.out.println("print param");
break;
case "String":
System.out.println("print String");
break;
case "null":
System.out.println("print null");
break;
default:
System.out.println("second default print");
}
}
}
本文部分参考文章:
【17】Java深入了解 if 和 switch 语句?switch 的底层数据结构?switch 是如何匹配枚举和字符串的? - 知乎 (zhihu.com)
2023.9.25备注: 在最新发布的JDK21 LTS中, 正式引入了增强模式匹配, 这部分内容待后续有时间了再看看(可能)...