在 Java 中要进行多条件判断时,我们通常会选择 if-else
或 switch
语句,比如在下面这个例子中,我们想要判断今天是不是周日或周末,
使用 if-else
可以这样编写:
int dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
if (dayOfWeek == Calendar.SUNDAY) {
System.out.println("Today is Sunday");
} else if (dayOfWeek == Calendar.SATURDAY) {
System.out.println("Today is Saturday");
} else {
System.out.println("Today is not Sunday or Saturday");
}
而使用 switch
则可以这样编写:
switch (dayOfWeek) {
case Calendar.SUNDAY -> System.out.println("Today is Sunday");
case Calendar.SATURDAY -> System.out.println("Today is Saturday");
default -> System.out.println("Today is not Sunday or Saturday");
}
一般来说下,我们认为 switch
语句比 if-else
更加高效,本文将将解释为何如此。
跳转指令
程序最后都是一条条的指令,CPU 有一个指令指示器,指向下一条要执行的指令,CPU 根据指令器的指示加载指令并执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一条指令。
但是有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让 CPU 跳到一个指定的地方执行。跳转有 2 种:一种是条件跳转,另一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。
if 语句的跳转指令
我们将上文的 if-else
语句编译后,通过 javap -c className
查看它的字节码:
0: invokestatic #7 // Method java/util/Calendar.getInstance:()Ljava/util/Calendar;
3: bipush 7
5: invokevirtual #13 // Method java/util/Calendar.get:(I)I
8: istore_1
9: iload_1
10: iconst_1
// 条件判断
11: if_icmpne 25
14: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
17: ldc #23 // String Today is Sunday
19: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 跳转指令
22: goto 50
25: iload_1
26: bipush 7
// 条件判断
28: if_icmpne 42
31: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #31 // String Today is Saturday
36: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 跳转指令
39: goto 50
42: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #33 // String Today is not Sunday or Saturday
47: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: return
我们看到有一系列的条件判断指令 if_icmpne
和跳转指令 goto
, CPU 根据这些指令跳转到相应的指令地址。
switch 语句的跳转指令
而对于 switch
,情况则有所不同,同样用 javap
命令查看编译后的 switch
语句,结果如下:
51: lookupswitch { // 2
1: 76
7: 87
default: 98
}
76: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
79: ldc #23 // String Today is Sunday
81: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
84: goto 106
87: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
90: ldc #31 // String Today is Saturday
92: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
95: goto 106
98: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
101: ldc #33 // String Today is not Sunday or Saturday
103: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
106: return
我们可以看到编译器根据 switch
的条件值,生成了一个跳转表,并使用 lookupswitch
指令查找跳转地址,CPU 在执行时,根据跳转表的条件值通过二分查找法找到要跳转的指令地址。
需要注意的是,lookupswitch
所包含的跳转表是有序的,这是编译器在编译阶段做的优化。即使我们将 switch 中的条件值顺序打乱,编译器在编译时仍会对条件值进行排序。由于值有序,lookupswitch
使用二分法查找跳转地址的效率为 O(log(n))
.
switch (dayOfWeek) {
case Calendar.SATURDAY -> System.out.println("Today is Saturday");
case Calendar.MONDAY -> System.out.println("Today is Monday");
case Calendar.SUNDAY -> System.out.println("Today is Sunday");
default -> System.out.println("Today is not Sunday or Saturday");
}
107: lookupswitch { // 3
1: 162
2: 151
7: 140
default: 173
}
如果值是连续的,跳转表还会进行特殊的优化,优化为一个数组,值就是数组的下标索引,直接根据值就可以找到跳转的地址。
switch (dayOfWeek) {
case Calendar.SATURDAY -> System.out.println("Today is Saturday");
case Calendar.MONDAY -> System.out.println("Today is Monday");
case Calendar.THURSDAY -> System.out.println("Today is Thursday");
case Calendar.WEDNESDAY -> System.out.println("Today is Wednesday");
case Calendar.FRIDAY -> System.out.println("Today is Friday");
case Calendar.TUESDAY -> System.out.println("Today is Tuesday");
case Calendar.SUNDAY -> System.out.println("Today is Sunday");
default -> System.out.println("Today is not Sunday or Saturday");
}
107: tableswitch { // 1 to 7
1: 214
2: 159
3: 203
4: 181
5: 170
6: 192
7: 148
default: 225
}
总结
总得来说,由于跳转表的存在,CPU 在执行 switch
语句时需要进行的条件判断次数可能更少,因此 switch
语句的执行效率一般高于 if-else
.