zoukankan      html  css  js  c++  java
  • 深入理解Java的switch...case...语句

    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,那么对应的字节码就会“滑落”到下一行语句,继续执行。

    附1——idea查看字节码方法

    Mac下preference->Tools->External Tools,点击+,按如下页面配置即可。

    Windows下需要将上图填入的javap改为javap.exe。

    注意:每次查看字节码前,要确保对应类被重新编译,才能看到最新版。

    附2——JDK7或8下,switch...case...使用字符串常量编译报错解决方式

    这种情况的真实原因是,JDK设置不一致,IDE没有完全使用预期的编译器版本。
    在IDEA里可以这样解决:
    Project Settings -> Project 设置项目语言
    如果仍未解决,检查
    File -> Project Structure -> Modules, 查看所有模块是否都是预期的等级。
    还有一处也可以看下File -> Settings -> Compiler -> Java Compiler. 这里可以设置项目及模块的编译器版本。

    备注

    文中所有log n均为以2为底n的对数。
    本文的写作契机是参加公司的XX安全学习,提到了switch...case...和if...else...的性能有差异,因此花了一天研究了一番。

    参考文档

    通过字节码分析java中的switch语句
    Difference between JVM's LookupSwitch and TableSwitch?
    IntelliJ switch statement using Strings error: use -source 7
    Intellij idea快速查看Java类字节码

  • 相关阅读:
    quick-cocos2d-x游戏开发【5】——创建菜单
    cocos2d-x3.0 lua学习(一个)
    hdu 3001 Travelling (TSP问题 )
    朱重组成功,?(行家都知道,几乎回答)
    无人机DLG生产作业流程
    自己定制个人无人机需要的准备工作的内容
    C# 多线程网络爬虫
    [转] c#中 多线程访问winform控件
    C# WinForm中 让控件全屏显示的实现代码
    5.数据绑定和表单标签库
  • 原文地址:https://www.cnblogs.com/wuyuegb2312/p/11172440.html
Copyright © 2011-2022 走看看