前段时间看见一道java的基础题,到现在时间过去的有点久,一模一样的原题是记不清了,还有个模糊印象,大概是这样:
public static void main(String[] args) {
int i = 4;
switch (i) {
default:
System.out.println("default");
case 1:
System.out.println("i = 1");
case 2:
System.out.println("i = 2");
break;
case 31:
System.out.println("i = 31");
break;
}
}
输出是多少?
emmmm~~~~~~,我承认,这题是很简单很基础,虽然我很不好意思,但是我看见这道题是那一秒,有点懵。这么多年写代码,default一般都是放在最后,要么就可能不写,这是第一次看见,把default放最前面。
当时思索了下,答案应该是输出:
default
i = 1
i = 2
到i=2这里时,遇到break就跳出去了。
后来验证了下,确实是这个答案。
后来反省了下,为什么会出现类似我这种状态。在我看来,有的人遇到这道题马上能看出正确答案,有的可能跟我差不多,先懵一下。我想自己虽然已经写了几年java,会出现这种情况,应该是我最初的认知加固有的思维导致的。
我平常一直认为,case就是从上往下比对,直到遇到匹配的,处理,然后有break就break,没有继续往下走直到遇到break;或者从上往下都没匹配到,就到default了。一直都是这样理解的,所以当时第一眼看到default在最上面然后懵的时候,心理想的是:我去,它怎么一开始就知道后面匹配不到,要走到default,再加上i=1后面没有break的影响,然后想的是,莫非最后要走完再走回去到default那里?
随后看了下字节码,明白原因了。
先看下我们常规写法:
public static void main(String[] args) {
int i = 4;
switch (i) {
case 1:
System.out.println("i = 1");
case 2:
System.out.println("i = 2");
break;
case 31:
System.out.println("i = 31");
break;
default:
System.out.println("default");
}
}
这样写,我觉得搁谁,不用想都知道输出:default。
看下它的字节码(这里不讨论关于switch的tableswitch和lookupswitch的问题,本来最后一个是case 3,为了避免常规写法编译后为tableswitch,我特意改为31,让数值大点,都是lookupswitch,减少要讨论的点。如果是tableswitch的话,确实是不用顺序比较,直接就跳到default对应的位置了):
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: istore_1
2: iload_1
3: lookupswitch { // 3
1: 36
2: 44
31: 55
default: 66
}
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String i = 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 #5 // String i = 2
49: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
52: goto 74
55: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
58: ldc #6 // String i = 3
60: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
63: goto 74
66: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
69: ldc #7 // String default
71: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
74: return
}
从上面偏移量为3的指令:lookupswitch那里,case 1,2,31和default对应到的指令偏移量是递增的,所以最后到defautl跳转到偏移量66处。最后执行完就return了。
看下default在最上面的字节码:
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: istore_1
2: iload_1
3: lookupswitch { // 3
1: 44
2: 52
31: 63
default: 36
}
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String default
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 #5 // String i = 1
49: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
52: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
55: ldc #6 // String i = 2
57: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: goto 71
63: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
66: ldc #7 // String i = 31
68: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
71: return
}
可以看到它的lookupswitch那里,default还是最后的,会顺序比较完,但是default后面指向的指令偏移量在其它几种情况前面,和源码位置对应,所以会从偏移量36处理开始执行,因为case 1没有break,向下滑落,一直到打印完case 2才goto到return。
所以,,,就是这么简单的道理~~~,尽管把default放最前面,编译后lookupswitch这种情况,default还是到最后,其它比较完了都不匹配的处理情况。
另外,在看字节码测试的过程中,发现了一个很有意思的现象,如果只有case 1,2(2种情况)和default这个场景,编译后都是lookupswitch;如果有case1,2,3(3种情况)和default,编译后为tableswitch;所以我才把case 设置1,2,31的时候,编译后还是lookupswitch。
关于选择tableswitch和lookupswitch的实现部分,这是jdk8的源码:http://hg.openjdk.java.net/jdk8/jdk8/langtools/file/30db5e0aaf83/src/share/classes/com/sun/tools/javac/jvm/Gen.java#l1129
下面,摘了关键的一部分:
// Compute number of labels and minimum and maximum label values.
// For each case, store its label in an array.
int lo = Integer.MAX_VALUE; // minimum label.
int hi = Integer.MIN_VALUE; // maximum label.
int nlabels = 0; // number of labels.
int[] labels = new int[cases.length()]; // the label array.
int defaultIndex = -1; // the index of the default clause.
List l = cases;
for (int i = 0; i < labels.length; i++) {
if (l.head.pat != null) {
int val = ((Number)l.head.pat.type.constValue()).intValue();
labels[i] = val;
if (val < lo) lo = val;
if (hi < val) hi = val;
nlabels++;
} else {
assert defaultIndex == -1;
defaultIndex = i;
}
l = l.tail;
}
// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
// 关键是这里选择是table还是lookupswitch
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;
看我中文注释的地方:
这里计算一下,如果 是case 1,2 的时候,这里case label没有default。如果是1,2的从上面往下算是,hi是2,lo是1,nlabels是2,所以:
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;
这里判断就是:opcode = 2 > 0 && 6 + 3 * 3 <= 7 + 3 * 2 ? tableswitch: lookupswitch。返回的是lookupswitch。
如果是case 1,2,3就是:opcode = 3 > 0 && 7 + 3 * 3 <= 9 + 3 * 3 ? tableswitch: lookupswitch。返回的是tableswitch。
如果是case 1,2,31就是:opcode = 3 > 0 && 35 + 3 * 3 <= 9 + 3 * 3 ? tableswitch: lookupswitch。返回的是lookupswitch。
这个计算不复杂,其它情况自己比对。
关于tableswitch和lookupswitch不是关注点就不再多说。