java关于switch的一道基础题

前段时间看见一道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<JCCase> 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不是关注点就不再多说。

猜你喜欢

转载自blog.csdn.net/x763795151/article/details/108030273