下载安卓APP箭头
箭头给我发消息

客服QQ:3315713922

深入理解Java的switch...case...语句

作者:课课家教育     来源: http://www.kokojia.com点击数:1262发布时间: 2019-07-16 10:11:06

标签: java初级程序员 JavaSwing Java

大神带你学编程,欢迎选课

switch...case...中条件表达式的演进

  • 最早时,只支持int、char、byte、short这样的整型的基本类型或对应的包装类型Integer、Character、Byte、Short常量
  • JDk1.5开始支持enum,原理是给枚举值进行了内部的编号,进行编号和枚举值的映射
  • 1.7开始支持String,但不允许为null。(原因可以看后文)

case表达式仅限字面值常量吗?

case表达式既可以用字面值常量,也可以用final修饰且初始化过的变量。例如以下代码可正常编译并执行:

    public static int test(int i) {
        final int j = 2;
        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case j:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

但是没有初始化就不行,比如下面的代码就无法通过编译

public class SwitchTest {

    private final int caseJ;

    public int test(int i) {
        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case caseJ:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

    SwitchTest(int caseJ) {
        this.caseJ = caseJ;
    }

    public static void main(String[] args) {
        SwitchTest testJ = new SwitchTest(1);
        System.out.print(testJ.test(2));
    }
}

lookupswitch和tableswitch

下面两种几乎一样的代码,会编译出大相径庭的字节码。

lookupswitch

    public static int test(int i) {

        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case 2:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

对应字节码

  public static int test(int);
    Code:
       0: iload_0
       1: lookupswitch  { // 3
                     0: 36
                     2: 41
                    10: 46
               default: 51
          }
      36: iconst_0
      37: istore_1
      38: goto          53
      41: iconst_1
      42: istore_1
      43: goto          53
      46: iconst_4
      47: istore_1
      48: goto          53
      51: iconst_m1
      52: istore_1
      53: iload_1
      54: ireturn

tableswitch

    public static int test(int i) {

        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case 2:
                result = 1;
                break;
            case 4:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }
  public static int test(int);
    Code:
       0: iload_0
       1: tableswitch   { // 0 to 4
                     0: 36
                     1: 51
                     2: 41
                     3: 51
                     4: 46
               default: 51
          }
      36: iconst_0
      37: istore_1
      38: goto          53
      41: iconst_1
      42: istore_1
      43: goto          53
      46: iconst_4
      47: istore_1
      48: goto          53
      51: iconst_m1
      52: istore_1
      53: iload_1
      54: ireturn

两种字节码,最大的区别是执行了不同的指令:lookupswitch和tableswitch。

两种switch区别

  • tableswitch使用了一个数组,通过下标可以直接定位到要跳转的行。但是在生成字节码时,有的行可能在源码中并不存在。通过这种方式可以获得O(1)的时间复杂度。
  • lookupswitch维护了一个key-value的关系,通过逐个比较索引来查找匹配的待跳转的行数。而查找最好的性能是O(log n),如二分查找。
    可见,通过用冗余的机器码,tableswitch换取了更好的性能。

但是,在分支比较少的情况下,O(log n)其实并不大。n=2时,log n 约为2.8;即使n=100, log n 约为 6.6,与1仍未达到1个数量级的差距。

何时生成tableswitch?何时生成lookupswitch?

在JDK1.8环境下,通过检索langtools这个包,可以在langtools/src/share/classes/com/sun/tools/javac/jvm/Gen.java看到以下代码:

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;

这段代码的上下文:

  • hi和lo分别代表值的上下限,是通过遍历switch...case...每个分支获取的。
  • nlabels表示switch...case...的分支个数

可以看出,决策的条件综合考虑了时间复杂度(table_time_cost/lookup_time_cost)和空间复杂度(table_space_cost/lookup_space_cost),并且时间复杂度的权重是空间复杂度的3倍。

存疑点:

  • 各种幻数没有解释取值的原因,比如4、3,应该和具体细节实现有关。
  • lookupswitch的时间复杂度使用的是nlabels而没有取log n。此处可以看做是近似计算。

switch...case...优于if...else...吗?

一般来说,更多的限制能带来更好的性能。
从上文可以看出,无论是tableswitch还是lookupswitch,都有对随机查找的优化,而if...else...是没有的,可以看下面的源码和字节码。

    public static int test2(int i) {

        int result;
        if(i == 0) {
            result = 0;
        } else if(i == 1) {
            result = 1;
        } else if(i == 4) {
            result = 4;
        } else {
            result = -1;
        }
        return result;
    }
  public static int test2(int);
    Code:
       0: iload_0
       1: ifne          9
       4: iconst_0
       5: istore_1
       6: goto          31
       9: iload_0
      10: iconst_1
      11: if_icmpne     19
      14: iconst_1
      15: istore_1
      16: goto          31
      19: iload_0
      20: iconst_4
      21: if_icmpne     29
      24: iconst_4
      25: istore_1
      26: goto          31
      29: iconst_m1
      30: istore_1
      31: iload_1
      32: ireturn

字符串常量的case表达式及字节码

举例如下,这段源码有两个特点:

  1. case "ghi"分支里是没有赋值代码
  2. case "test"分支和case "test2"分支相同
    public static int testString(String str) {

        int result = -4;
        switch (str) {
            case "abc":
                result = 0;
                break;
            case "def":
                result = 1;
                break;
            case "ghi":
                break;
            case "test":
            case "test2":
                result = 1;
                break;
            default:
                result = -1;
        }
        return result;
    }

对应字节码

  public static int testString(java.lang.String);
    Code:
       0: bipush        -4
       2: istore_1
       3: aload_0
       4: astore_2
       5: iconst_m1
       6: istore_3
       7: aload_2
       8: invokevirtual #2                  // Method java/lang/String.hashCode:()I
      11: lookupswitch  { // 5
                 96354: 60
                 99333: 74
                102312: 88
               3556498: 102
             110251488: 116
               default: 127
          }
      60: aload_2
      61: ldc           #3                  // String abc
      63: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      66: ifeq          127
      69: iconst_0
      70: istore_3
      71: goto          127
      74: aload_2
      75: ldc           #5                  // String def
      77: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      80: ifeq          127
      83: iconst_1
      84: istore_3
      85: goto          127
      88: aload_2
      89: ldc           #6                  // String ghi
      91: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      94: ifeq          127
      97: iconst_2
      98: istore_3
      99: goto          127
     102: aload_2
     103: ldc           #7                  // String test
     105: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     108: ifeq          127
     111: iconst_3
     112: istore_3
     113: goto          127
     116: aload_2
     117: ldc           #8                  // String test2
     119: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     122: ifeq          127
     125: iconst_4
     126: istore_3
     127: iload_3
     128: tableswitch   { // 0 to 4
                     0: 164
                     1: 169
                     2: 174
                     3: 177
                     4: 177
               default: 182
          }
     164: iconst_0
     165: istore_1
     166: goto          184
     169: iconst_1
     170: istore_1
     171: goto          184
     174: goto          184
     177: iconst_1
     178: istore_1
     179: goto          184
     182: iconst_m1
     183: istore_1
     184: iload_1
     185: ireturn

可以看到与整型常量的不同:

  1. String常量判等,先计算hashCode,在lookupswitch分支中再比较是否真正相等。这也是不支持null的原因,此时hashCode无法计算。
  2. lookupswitch分支中,会给每个分支分配一个新下标值,作为后面的tableswitch的索引。源码中的分支语句统一在tableswitch中对应分支执行。

为什么要再生成一段tableswitch?从字节码来看,两个平行的分支("test"和"test2"),虽然没有在tableswitch中用同一个数组下标,但是使用了同一个跳转行177,在这种情况下减少了字节码冗余。

枚举的case表达式及字节码

样例代码如下

    public static int testEnum(StatusEnum statusEnum) {

        int result;
        switch (statusEnum) {
            case INIT:
                result = 0;
                break;
            case FINISH:
                result = 1;
                break;
            default:
                result = -1;
        }
        return result;
    }

对应字节码

  public static int testEnum(com.example.StatusEnum);
    Code:
       0: getstatic     #9                  // Field com/example/SwitchTest$1.$SwitchMap$com$example$core$service$domain$enums$StatusEnum:[I
       3: aload_0
       4: invokevirtual #10                 // Method com/example/core/service/domain/enums/StatusEnum.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 41
               default: 46
          }
      36: iconst_0
      37: istore_1
      38: goto          48
      41: iconst_1
      42: istore_1
      43: goto          48
      46: iconst_m1
      47: istore_1
      48: iload_1
      49: ireturn

可以看到,使用了枚举的ordinal方法确定序号。

其他

通过查看字节码,可以发现源码的break关键字,对应的是字节码goto到具体行的语句。 如果不用break,那么对应的字节码就会“滑落”到下一行语句,继续执行。

深入理解Java的switch...case...语句_java初级程序员_ JavaSwing _Java _课课家

赞(17)
踩(0)
分享到:
华为认证网络工程师 HCIE直播课视频教程