实现原理
语言的底层就是算法,所以switch-case的底层也是算法: 数组和二分查找。
switch-case是一个条件语句,也就是说: 如果满足条件,那么就执行对应的指令,也就是: 找条件!那么就是查找!也就是算法里的查找!
那么为什么是数组和二分查找呢?
其实switch-case的状态值只能存放一个int的大小,比如byte,char,shor,int等。如果大于一个int的大小就不可以,比如long,double等。那么String为什么也可以呢?因为switch(String)会取String对应的hashcode,而hashcode正好是一个int的大小。
正是因为状态值是一个int的大小,所以可以对状态值进行排序,「如果状态值是紧凑的,那么就会将状态值优化为tableswitch(可以理解为数组),查找效率就是O(1)的;如果状态值是分散的,那么就会优化为lookupswitch,然后使用二分查找来找条件。」
现在我们来证明,假设有如下代码:
public void test(int a){
int b = 10;
switch(a) {
case 1:
b = 1;
break;
case 2:
b = 2;
break;
case 3:
b = 3;
break;
default:
b = 4;
break;
}
}
我们使用javac指令得到Test.class对象,然后使用javap -verbose Test.class得到字节码如下(这里只粘出关键部分):
public void test(int);
descriptor: (I)V // 表示函数参数类型是int,返回类型是void
flags: (0x0001) ACC_PUBLIC // 表示函数是public的
Code:
stack=1, locals=3, args_size=2 // 栈深度为1,局部变量表为2,参数个数为2
0: bipush 10
2: istore_2
3: iload_1
4: tableswitch { // 1 to 3 看这里,优化成了tableswitch
1: 32 // 表示case到1就执行第32行字节码
2: 37
3: 42
default: 47
}
可以看到,我们确实是优化成了tableswitch,因为我们case的值是"连续的",连续的肯定是紧凑的,那么紧凑必须是连续的吗?非也!我们修改代码如下:
public void test(int a){
int b = 10;
switch(a) {
case 1:
b = 1;
break;
case 3:
b = 3;
break;
case 5:
b = 5;
break;
default:
b = 4;
break;
}
}
现在,我们的case值是1、3、5,不连续了,我们继续javap一下来看字节码:
public void test(int);
descriptor: (I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=2
0: bipush 10
2: istore_2
3: iload_1
4: tableswitch { // 1 to 5 这里自动补上了2和4
1: 40
2: 55 // 执行第55行字节码
3: 45
4: 55 // 也是执行第55行字节码
5: 50
default: 55 // 也是执行第55行字节码
}
可以看到,这里自动补上了2和4,变为连续的了,为的就是凑齐一个数组,然后可以直接根据下标进行查找定位,从而达到O(1)的时间复杂度。而且2和4跟default一样,都执行第55行字节码,因为本来就没有2和4,就按default处理。
现在,让我们修改case 3为case 100,其他不变,然后看字节码:
public void test(int);
descriptor: (I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=2
0: bipush 10
2: istore_2
3: iload_1
4: lookupswitch { // 3 变为lookupswitch了
1: 40
5: 51
100: 45
default: 56
}
我们发现,现在变为lookupswitch了,并且还发现,case的值按照顺序排序了,这是为了使用二分查找。
因为从5到100断层比较严重,我们不可能为了O(1)的时间复杂度去插入95个值,可以看出JVM在时间和空间还是衡量的比较人性化的。
这里需要注意两点:
tableswitch的生成的条件是"紧凑"而不是"数值小",比如10001,10002,10003也会优化成tableswitch,而1,100,200就只能优化成lookupswitch。 如果case的值是String,那么会优化成lookupswitch,因为hashcode一般都是比较分散的(避免碰撞嘛)。
对String的特殊处理
上面我们说到,对String的处理是通过hashcode的。那么,如果两个String的hashcode相同会怎么样呢?比如"Aa"和"BB",他们的hashcode都是2112,那么如下函数:
public void test(String a) {
int b = 10;
switch(a) {
case "Aa":
b = 1;
break;
case "BB":
b = 100;
break;
default:
b = 1000;
break;
}
}
是不是传入"Aa"和"BB"都一样呢?不会!
因为如果switch的状态值是String的时候,除了进行hashcode校验,还会使用equals进行值校验,类似于HashMap中的定位逻辑,等价代码如下:
public void test(String a) {
int b = 10;
switch(a.hashCode()) { // 这里使用hashcode进行检索
case 2112: // 转换为hashcode进行检索
if(a.equals("Aa")) { // hashcode命中了,就是用equals来进行二次定位
b = 1;
}else if(a.equals("BB")) {
b = 100;
}
break;
default:
b = 1000;
break;
}
}
可以看到,switch-case在条件值是String的时候,会使用hashcode作为switch-case的状态值,当hashcode相同的时候,再使用equals来进行二次比较,只有hashcode和equals都相同才说明复合条件,「这属于分页思想:粗略定位到精准定位」。
「Tips:并不是只有hashcode相同才使用equals比较,而是不管hashcode是否相同,都会使用equals进行比较。」
我们将上述代码编译下,来看字节码:
public void test(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: bipush 10
2: istore_2
3: aload_1
4: astore_3
5: iconst_m1
6: istore 4
8: aload_3
9: invokevirtual #7 // Method java/lang/String.hashCode:()I // 这里调用了String的hashCode()
12: lookupswitch { // 1
2112: 32 // 只有一个2112,说明合并了,32表示执行第32行字节码
default: 59 // 否则就执行第59行字节码
}
我们接着来看字节码指令(只看注释部分即可),对字节码指令不熟悉可以看这里:
32: aload_3
33: ldc #13 // String BB // 将BB推送至栈顶
35: invokevirtual #15 // Method java/lang/String.equals:(Ljava/lang/Object;)Z // 执行equals语句
38: ifeq 47 // 如果相同(if equals)
41: iconst_1
42: istore 4
44: goto 59
47: aload_3
48: ldc #19 // String Aa // jiangAa推送值栈顶
50: invokevirtual #15 // Method java/lang/String.equals:(Ljava/lang/Object;)Z // 执行equals语句
53: ifeq 59 // 如果相同
56: iconst_0
57: istore 4
59: iload 4
61: lookupswitch { // 2
0: 88
1: 93
default: 99
}
88: iconst_1
89: istore_2
90: goto 103
93: bipush 100
95: istore_2
96: goto 103
99: sipush 1000
102: istore_2
103: return
上述代码验证了我们的结论: 「switch-case条件是String时候,使用String的hashCode来作为状态值,如果相同就合并,内部使用equals进行二次比较。」
优化
明白了上述原理,我们怎么在代码中优化呢?很简单!
第一: 「我们尽可能的让case的值连续,或者紧凑,千万别为了装x而xjb写,而且尽量不使用String(因为hashcode本来就比较分散)」。
第二: 「如果业务比较复杂,或者出于拓展性考虑,那么可以使用hashmap来优化,毕竟hashmap的时间复杂度大部分情况都是O(1)的,而且将来修改也满足OCP」。
比如上述代码我们就可以这样用hashmap来优化:
// 首先定义一个Action接口:
public interface Action {
int action();
}
// 然后初始化
private HashMap<Integer, Action> actions = new HashMap<>();
{
actions.put(1, () -> 1); // case 1
actions.put(3, () -> 3); // case 3
actions.put(5, () -> 5); // case 5
}
// test函数就可以简写为如下:
public void test(int a) {
int b = actions.get(a).action();
}
将来要添加case或删除case,只要在actions中添加或删除值就行,美滋滋。
综上,我们可以知道:
1 建议优先使用hashmap来处理switch-case这种复合条件的场景。 2 如果条件比较简单,或者不必考虑拓展性,可以使用swtich-case,但是建议 非必要不使用 String来作为case值,而且建议case值一定要紧凑。




