为什么要写这篇文章?

今儿写代码时,一位前辈看到了我满篇的if+else if,他告诉我说:尽量使用switch来代替if+else ifif+else if效率比switch低,会增加无用的判断

年轻的我差点就信了!

那么switchif+else if的效率问题真的就如他所言?对我来说,这真的是个很有趣的问题!

同时我也想要知道,在我们实际开发中,什么时候用switch,什么时候用if+else if合适呢?

再后来,就引发了我更多的思考,switch它到底拿着我的判断条件是怎么操作的?

人年纪大了,什么都想问个为什么,这也是我想要写这篇文章的一个动机。

你有没有想过switch内部是怎么实现的?

就以下几种switch不同值的情况来进行分析吧!

1、switch的case值是连续的。

我就先端一盘代码出来吧,switch的正常代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestSwitch {

public int testSwitch(int t) {
int result= 0;
switch (t) {
case 0:
result= 100;
break;
case 1:
result= 200;
break;
case 2:
result= 300;
break;
}
return result;
}
}

这上面是一份很本分的switch代码,现在我们来看一下该代码在反汇编后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class com.tang.demoapplication.TestPart.TestSwitch {
public com.tang.demoapplication.TestPart.TestSwitch();
Code:
0: aload_0 //将this引用推送至栈顶,即压入栈
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int testSwitch(int);
Code:
0: iconst_0
1: istore_2
2: iload_1
3: tableswitch { // A: 0 to 2 :从栈顶中弹出元素,检查是否在[0,2]之内
0: 28 //当code=0时,跳到28的位置,返回值100
1: 34 //当code=1时,跳到34的位置,返回值200
2: 41 //当code=2时,跳到41的位置,返回值300
default: 45 // B: 如果不在[0,2]内,则程序计数器跳转到第45行
}
28: bipush 100 //将常量100压入栈顶
30: istore_2 //将栈顶元素10存入局部变量表的第3个位置上
31: goto 45 //跳转到45行
34: sipush 200
37: istore_2 //将一个数值从操作数栈存储到局部变量表
38: goto 45
41: sipush 300
44: istore_2
45: iload_2
46: ireturn
}

Tips:JVM字节码命令

  • istore_n:将一个数值从操作数栈存储到局部变量表。
    举例:istore_0 将栈顶int型数值存入第一个本地变量;lstore_0 将栈顶long型数值存入第一个本地变量,float、double同理
  • iload_n:将一个局部变量加载到操作栈。
    举例:iload_0将第一个int型本地变量推送至栈顶;lload_0将第一个long型本地变量推送至栈顶,float、double同理
  • sipush:将一个常量加载到操作数栈
  • iconst_i:将一个常量加载到操作数栈。
    举例:iconst_0 将int型(0)推送至栈顶;lconst_0 将long型(0)推送至栈顶,float、double同理

你先记住上面这个 tableswitch ,它身上戏有点多。它要和下面的不连续的情况来进行对比,你才能更加深刻地感受到两者的差异!

2、switch的case值不是连续的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestSwitch {
public int testSwitch(int t) {
int result = 0;
switch (t) {
case 0:
result = 100;
break;
case 4:
result = 200;
break;
case 9:
result = 300;
break;
}
return result ;
}
}

汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class com.tang.demoapplication.TestPart.TestSwitch {
public com.tang.demoapplication.TestPart.TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int testSwitch(int);
Code:
0: iconst_0
1: istore_2
2: iload_1
3: lookupswitch { //!!注意此时是lookupswitch
0: 36 //当值为0时,跳转到36行,返回值100
4: 42 //当值为4时,跳转到42行,返回值200
9: 49
default: 53
}
36: bipush 100
38: istore_2
39: goto 53
42: sipush 200
45: istore_2
46: goto 53
49: sipush 300
52: istore_2
53: iload_2
54: ireturn
}

(“上面是汇编,看不懂很正常!”,我自言自语道。)


好的,出现了,一个新名词:lookupswitch

显而易见,当switch的值为有序的时候,用的是tableswitch;而当switch的值是无序的时候,用的是lookupswitch

tableswitch

  • 它会进行范围检查,检查不通过则直接执行default。如果检查通过,则执行相应的case;
  • 它使用数据结构存储偏移量,可利用下标快速定位到偏移量(因为是连续的,可以想象一下吧)。

lookupswitch

  • 它是无需时会使用。当条件大面积不连续时,lookupswitch会产生大量的额外空间;
  • 使用lookupswitch,会将case进行排序,然后将值拿进去二分法查找对应的分支偏移量

3、switch的case类型为String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestSwitch {
public int testSwitch(String t) {
int result = 0;
switch (t) {
case "a":
result = 100;
break;
case "d":
result = 200;
break;
case "f":
result = 300;
break;
}
return result ;
}
}

class编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestSwitch {
public TestSwitch() {
}

public int testSwitch(String t) {
int result = 0;
byte var4 = -1;
switch(t.hashCode()) {
case 97:
if (t.equals("a")) {
var4 = 0;
}
break;
case 100:
if (t.equals("d")) {
var4 = 1;
}
break;
case 102:
if (t.equals("f")) {
var4 = 2;
}
}

switch(var4) {
case 0:
result = 100;
break;
case 1:
result = 200;
break;
case 2:
result = 300;
}

return result ;
}
}

汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class com.tang.demoapplication.TestPart.TestSwitch {
public com.tang.demoapplication.TestPart.TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int testSwitch(java.lang.String);
Code:
0: iconst_0
1: istore_2
2: aload_1
3: astore_3
4: iconst_m1
5: istore 4
7: aload_3
8: invokevirtual #2 // Method java/lang/String.hashCode:()I
11: lookupswitch { //这里先将‘a’,'d','f'转化为hashcode
97: 44
100: 59
102: 74
default: 86
}
44: aload_3 //将第四个引用类型本地变量推送至栈顶
45: ldc #3 // String a ldc:将int, float或String型常量值从常量池中推送至栈顶
47: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
50: ifeq 86
53: iconst_0
54: istore 4
56: goto 86
59: aload_3
60: ldc #5 // String d
62: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
65: ifeq 86
68: iconst_1
69: istore 4
71: goto 86
74: aload_3
75: ldc #6 // String f
77: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
80: ifeq 86
83: iconst_2
84: istore 4
86: iload 4
88: tableswitch { // 0 to 2
0: 116
1: 122
2: 129
default: 133
}
116: bipush 100
118: istore_2
119: goto 133
122: sipush 200
125: istore_2
126: goto 133
129: sipush 300
132: istore_2
133: iload_2
134: ireturn
}

这里你会看到:这不仅仅是只用了tableswitch或者lookupswitch,而是两者都使用到了。
啧,这可能就是男女搭配,干活不累吧。


上面的代码,大家肯定有众多疑问!

  1. JDK所谓的switch支持String是真的支持吗?

    针对String类型的switch,实际是取得该String字符串哈希值再进行的switch。
    说到底,实际JVM仍然还是不支持这种String参数的语法结构,我们可以看做是一颗语法糖。

  2. class代码中,增加了equals判断,这有啥用?

    这样能避免哈希冲突导致问题!

  3. 为何class底层会使用两个switch,明明一个就够了呀?

    假设底层只有一个switch,我们来设想一个场景:
    假如我们编写的代码使用100个case,且这些case都没有break。我们从上面知道,switch(String) 底层用到了equals判断。那么,在真正执行的时候,是不是就不得不执行一百次equals操作?

    答案是肯定的,所以,switch(String) 底层使用了两个switch,第一个switch只用于快速定位一个case,且会立马break(不管你编写的代码是否有break,它这儿都会break),第二个switch才进行具体的逻辑执行!

4、switch的case类型为枚举(enum )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestSwitch {
public enum TestEnum{
ENUM1,ENUM2,ENUM3
}

public int testSwitch(TestEnum t) {
int result = 0;
switch (t) {
case ENUM1:
result = 100;
break;
case ENUM2:
result = 200;
break;
case ENUM3:
result = 300;
break;
}
return resultNum;
}
}

汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class com.tang.demoapplication.TestPart.TestSwitch {
public com.tang.demoapplication.TestPart.TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int testSwitch(com.tang.demoapplication.TestPart.TestSwitch$TestEnum);
Code:
0: iconst_0
1: istore_2
2: getstatic #2 // Field com/tang/demoapplication/TestPart/TestSwitch$1.$SwitchMap$com$tang$demoapplication$TestPart$TestSwitch$TestEnum:[I
5: aload_1
6: invokevirtual #3 // Method com/tang/demoapplication/TestPart/TestSwitch$TestEnum.ordinal:()I
9: iaload
10: tableswitch { // 1 to 3
1: 36
2: 42
3: 49
default: 53
}
36: bipush 100
38: istore_2
39: goto 53
42: sipush 200
45: istore_2
46: goto 53
49: sipush 300
52: istore_2
53: iload_2
54: ireturn
}

不知道大家有没有注意到这句代码:
6: invokevirtual #3 // Method com/tang/demoapplication/TestPart/TestSwitch$TestEnum.ordinal:()I

这里面执行了ordinal(),那么这个方法是哪里来的呢?

带着问题,那我们接着往下看!

我们注意到编译该文件的时候,我们的文件多了几个class,TestSwitch1.classTestSwitchTestEnum.class,而TestSwitch.class是毋庸置疑就有的:

这里看到,多了两个文件——TestSwitch1.class和TestSwitchTestEnum.class
这里看到,多了两个文件——TestSwitch1.class和TestSwitchTestEnum.class

我们就看一下另外两个文件内容分别是什么吧!

TestSwitch$1.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestSwitch$1 {
static {
try {
$SwitchMap$com$tang$demoapplication$TestPart$TestSwitch$TestEnum[TestEnum.ENUM1.ordinal()] = 1;
} catch (NoSuchFieldError var3) {
}

try {
$SwitchMap$com$tang$demoapplication$TestPart$TestSwitch$TestEnum[TestEnum.ENUM2.ordinal()] = 2;
} catch (NoSuchFieldError var2) {
}

try {
$SwitchMap$com$tang$demoapplication$TestPart$TestSwitch$TestEnum[TestEnum.ENUM3.ordinal()] = 3;
} catch (NoSuchFieldError var1) {
}
}
}

TestSwitch$TestEnum.class(感觉这里面没什么彩蛋可以分析的):

1
2
3
4
5
6
7
8
public enum TestSwitch$TestEnum {
ENUM1,
ENUM2,
ENUM3;

private TestSwitch$TestEnum() {
}
}

好的,我们就先分析TestSwitch$1.class吧!

你应该注意到TestSwitch$1.class里面的ordinal()方法。那也就找到了我们反汇编内的ordinal()宿主。

也就是说:在这个TestSwitch$1.class里,声明了一个静态的数组,数组利用枚举的ordinal()值作为下标,数组中的元素依次递增。

那么该数组的作用是什么?

从汇编的代码可以看出:

  1. 我们首先获取到了静态数组
  2. 再调用枚举的ordinal()来获取枚举值
  3. 再将这个值作为静态数组的下标,获取这个静态数组中的某个值
  4. 再使用这个值去lookupswitch或tableswitch中去寻找值。

5、switch的case为包装类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestSwitch {
public int testSwitch(Byte i){
int result=0;
switch (i){
case 1:
result=100;
break;
case 2:
result=200;
break;
case 3:
result=300;
break;
}
return result;
}
}

汇编结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class com.tang.demoapplication.TestPart.TestSwitch {
public com.tang.demoapplication.TestPart.TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int testSwitch(java.lang.Byte);
Code:
0: iconst_0
1: istore_2
2: aload_1
3: invokevirtual #2 // Method java/lang/Byte.byteValue:()B (该处的byteValue是重点!)
6: tableswitch { // 1 to 3
1: 32
2: 38
3: 45
default: 49
}
32: bipush 100
34: istore_2
35: goto 49
38: sipush 200
41: istore_2
42: goto 49
45: sipush 300
48: istore_2
49: iload_2
50: ireturn
}

我们可以看到在进入tableswitch之前执行了一个byteValue()方法,该方法完成对byte的拆箱工作,然后比较byte值就行了。

switch小结

就上面的几种类型,这里进行汇总。

  • 当switch值为int时:数据是连续的,使用tableswitch进行判断;数据不是连续的,使用lookupswitch进行判断
  • 当switch值为String时:现将String值转换成hashcode,随后采用equal判断,用一个新值来进行tableswitch判断。
  • 当switch值为Enum时:自动生成SwitchMap数组,下标是枚举的ordinal()`,值是从1开始递增的整数。
  • 包装类型:先进行拆箱,然后tableswitch / lookupswitch判断。

switchif+else if的抉择

说到了这里,你是不是还是不知道什么时候switch,什么时候if+else if

根据大量的实际程序测试(不考虑不同的编译器优化程度差异,假设都是最好的优化),那么switch语句击中第三个选项的时间跟if+else if语句击中第三个选项的时间相同

击中第一,第二选项的速度if+else if语句快,击中第四以及第四之后的选项的速度switch语句快!

在实际开发中,到底你是用switch还是if+else if,其实影响没有特别大,本文纯属个人觉得有趣~

参考了大牛的博客,自己再手动来操作和观察,同时也咨询了一下小伙伴@localhost01,耗费了挺长的时间,感恩各位开路大牛。


更多文章,请关注:开猿笔记