揭秘Java switch语句中的case穿透现象
导语:在 Java 开发中,我们经常使用switch
语句来进行条件判断和分支选择。然而,有一个令人困惑的现象就是,当某个case
语句没有加上break
关键字时,程序会继续执行下一个case
语句,这被称为case穿透现象
。本文将揭秘case穿透现象
的原因,并解释为何会出现这种行为。
1. switch 语句简介
在开始揭秘case穿透现象
之前,我们先简单回顾一下switch
语句的基本用法。switch
语句用于根据变量的不同取值执行相应的代码块。其语法结构如下:
switch (expression) {
case value1:
// 执行代码块1
break;
case value2:
// 执行代码块2
break;
...
default:
// 默认代码块
}
switch case支持的6种数据类型:switch 表达式后面的数据类型只支持byte、short、int整形类型、字符类型char、枚举类型和java.lang.String类型。
根据expression
的值,程序会跳转到对应的case
语句进行匹配并执行相应的代码块,直到遇到break
关键字或者到达switch
语句的结尾。
如果某个case
语句没有break
,程序会继续执行下一个case语句,这就是case穿透现象
。
我们看下面这个例子。
public class Test {
public static void main(String[] args) {
int i = 0;
switch (i) {
case 0:
System.out.println("0");
case 1:
System.out.println("1");
case 2:
System.out.println("2");
}
}
}
打印结果:
0
1
2
2. case穿透现象的原因
按照惯用套路,看看字节码能不能给个答案。
javac编译
和javap查看
:
> javap -c -l Test.class
Compiled from "Test.java"
public class com.atu.algorithm.aTest.Test {
public com.atu.algorithm.aTest.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: tableswitch { // 0 to 2
0: 28
1: 36
2: 44
default: 52
}
28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #3 // String 0
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #5 // String 1
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
47: ldc #6 // String 2
49: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
52: return
LineNumberTable:
line 10: 0
line 11: 2
line 13: 28
line 15: 36
line 17: 44
line 19: 52
}
根据提供的字节码,我们来解释一下case穿透
的情况。
在main方法中,通过tableswitch
指令实现了一个switch
语句。switch
语句会根据值进行跳转,并执行对应的代码块。
在这个例子中,根据tableswitch
指令的参数 {0 to 2}
,case
的范围是从0到2。
- 当
switch
的表达式的值为0时,程序会跳转到标签为28的位置,然后继续执行28标签处的代码块。 - 为1时跳转到标号36代码处;
- 为2时跳转到标号44代码处;
- default则跳转到标号52代码处。
这不,答案就出来了,当case 0
匹配了之后,直接跳转到标号28代码处开始执行,输出0,然后策马奔腾,一路下坡,顺序执行完后面所有代码,直到标号52 return,方法完执行完成,程序结束。
如果按照正常的思维,是不是case 0
匹配之后,跳到28,执行完28、31、32输出0之后,就应该直接跳走,直接执行49。
那么,这个【跳走】用字节码应该怎么表示?
关于 goto
再写代码样例,这次我们在代码中给每个case
都加上break
。
public class Test {
public static void main(String[] args) {
int i = 0;
switch (i) {
case 0:
System.out.println("0");
break;
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
}
System.out.println("Hello World");
}
}
打印结果:
0
Hello World
重新编译,再来看看字节码。
Compiled from "Test.java"
public class com.atu.algorithm.aTest.Test {
public com.atu.algorithm.aTest.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: tableswitch { // 0 to 2
0: 28
1: 39
2: 50
default: 58
}
28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #3 // String 0
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: goto 58
39: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
42: ldc #5 // String 1
44: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
47: goto 58
50: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
53: ldc #6 // String 2
55: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #7 // String Hello World
63: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: return
LineNumberTable:
line 10: 0
line 11: 2
line 13: 28
line 14: 36
line 16: 39
line 17: 47
line 19: 50
line 22: 58
line 23: 66
}
如图,与第一次的字节码相比,在标号36、47都有了goto指令
。如果case 0
匹配成功,则跳到标号28执行,执行完代码块对应的31、33指令之后,执行36的goto指令
跳转到标号58,这样就跳出了switch
作用范围,case 1和2也不会被执行。
在Java字节码中,goto指令用于无条件跳转到指定的目标代码块。它可以实现程序的跳转和循环控制。
等等,怎么少了一个goto,在标号58的上方应该还有一个goto才对!
其实这就涉及到了编译器优化技术,最后一个goto也是跳转到标号58的指令,但没有goto下一步也一样顺序执行此行指令,所以这个goto被编译器视为无用代码进行了消除。
3. switch和if的区别
先用if
实现上面switch
逻辑。
public class Test {
public static void main(String[] args) {
int i = 0;
if (i == 0) {
System.out.println(0);
} else if (i == 1) {
System.out.println(1);
} else if (i == 2) {
System.out.println(2);
}
System.out.println("Hello World");
}
}
编译成字节码
Compiled from "Test.java"
public class com.atu.algorithm.aTest.Test {
public com.atu.algorithm.aTest.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 16
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
9: iconst_0
10: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
13: goto 43
16: iload_1
17: iconst_1
18: if_icmpne 31
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iconst_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: goto 43
31: iload_1
32: iconst_2
33: if_icmpne 43
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: iconst_2
40: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
43: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
46: ldc #4 // String Hello World
48: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
51: return
}
「ifne」和「if_icmpne」是Java字节码指令中的两个条件分支指令,用于在程序执行过程中进行条件判断并跳转到相应的代码块。它们的区别在于操作数类型和比较方式。
ifne
:操作数类型为int,功能是当栈顶元素不等于零时,跳转到指定的代码块。if_icmpne
:操作数类型为int,当两个int类型的数值不相等时,跳转到指定的代码块。
从字节码也可以看出if
和switch
的区别:
if
条件和代码块的字节码是顺序的,switch
条件和代码块是分开的;if
自动生成goto
指令,switch
只有加了break
才生成goto
指令。
4. 总结
case穿透现象
:指在switch语句中,当某个case
语句没有break
,程序会继续执行下一个case
语句。case
中的break
作用是告诉前端编译器:「给每个case
对应代码块的最后加上goto
」。这样,执行完匹配上的代码之后,就可以略过后面的case
代码块了。switch
都支持哪些类型呢?- 基本数据类型:byte, short, char, int
- 包装数据类型:Byte, Short, Character, Integer
- 枚举类型:Enum
- 字符串类型:String(Jdk 7+ 开始支持)