【JAVA】初探switch实现原理

日常编码中,我们常常用到switch语句,在我的另外一篇文章中【JAVA】优化if else的几种方式,也谈到了可以利用switch来优化if-else结构,那么switch底层究竟是如何实现的呢?


我们先写几个示例

第一个示例:case条件中的int值连续

public int switchInt(int i) { int result; switch (i) { case 0:
                result = 10; break; case 1:
                result = 20; break; case 2:
                result = 30; break; default:
                result = 40;
        } return result;
    }

老规矩,反编译后得到:

(首先javac Main.java,之后javap -c -p Main.class)

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1         // Method java/lang/Object."<init>":()V
       4: return

  public int switchInt(int);
    Code:
       0: iload_1                       //将局部变量表下标为1的元素,也就是传入的i的值压入栈顶
       1: tableswitch   { // 0 to 2     //从栈顶中弹出元素,检查是否在[0,2]之内
                     0: 28              //如果为0,则程序计数器跳转到第28行
                     1: 34              //如果为1,则程序计数器跳转到第34行              
                     2: 40              //如果为2,则程序计数器跳转到第40行
               default: 46              //如果不在[0,2]内,则程序计数器跳转到第46行
          }
      28: bipush        10              //将常量10压入栈顶
      30: istore_2                      //将栈顶元素10存入局部变量表的第3个位置上
      31: goto          49              //跳转到底49行
      34: bipush        20
      36: istore_2
      37: goto          49
      40: bipush        30
      42: istore_2
      43: goto          49
      46: bipush        40
      48: istore_2
      49: iload_2                       //将局部变量表中的第3个元素压入栈中
      50: ireturn                       //弹出栈顶元素,方法返回
}

其中的tableswitch是后面会着重分析的内容,这里先放着。

这里有一个疑问:

为什么刚开始传进来的i的值,是存到局部变量表中的第二个位置,第一个位置到底放了啥?

因为该方法是一个实例方法,局部变量表的第一个位置总是存放着this引用。在构造方法中,已经使用aload_0将this应用存入到了局部变量表中的第一个位置上。

关于这些字节码指令,可以参考这篇文章jvm理论-字节码指令


第二个示例:case中的int值不连续

public int switchInt(int i) { int result; switch (i) { case 0:
                result = 10; break; case 3:
                result = 20; break; case 7:
                result = 30; break; default:
                result = 40;
        } return result;
    }

现在改成0,3,7了,继续观察反编译后得到的内容:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchInt(int);
    Code:
       0: iload_1
       1: lookupswitch  { // 3
                     0: 36
                     3: 42
                     7: 48
               default: 54
          }
      36: bipush        10
      38: istore_2
      39: goto          57
      42: bipush        20
      44: istore_2
      45: goto          57
      48: bipush        30
      50: istore_2
      51: goto          57
      54: bipush        40
      56: istore_2
      57: iload_2
      58: ireturn
}

仅仅改动了case中的值,由连续改成了不连续,得到的字节码指令居然发生了变化,由tableswitch变成了lookupswitch


看到这里,我们可以总结一下

tableswitch和lookupswitch两者的区别

tableswitch

tableswitch用来处理条件连续的情况。首先进行一次范围检查,检查不通过便直接返回default。检查通过后,会得到对应case语句的偏移量。

tableswitch使用数组结构存储偏移量,因此利用下标可以快速定位到偏移量,时间复杂度为O(N)

注意:这里的“连续”可以理解为相对连续或半连续,012连续,013可以理解为半连续。013也会使用tableswitch,只不过里面缺少的2和default分支会有同样的偏移量。

lookupswitch

lookupswitch则用来处理条件不连续的情况,当条件大面积不连续时,tableswitch将会产生大量的额外空间。使用lookupswitch,会将case值进行排序,之后可以利用二分法快速查到对应的分支偏移量。

lookupswitch则是维护了一个经过key排序的(key,value)结构,查找复杂度一般为O(logN)


第三个示例:使用String类型

在jdk1.7的时候,可以使用String类型作为case条件了。

public int switchString(String str) { int result; switch (str) { case "a":
                result = 10; break; case "c":
                result = 20; break; case "f":
                result = 30; break; default:
                result = 40;
        } return result;
    }

这次我们不直接看字节码指令,仅仅观察class文件:

public class Main { public Main() {
    } public int switchString(String var1) { byte var4 = -1; switch(var1.hashCode()) { case 97: if (var1.equals("a")) {
                var4 = 0;
            } break; case 99: if (var1.equals("c")) {
                var4 = 1;
            } break; case 102: if (var1.equals("f")) {
                var4 = 2;
            }
        } byte var2; switch(var4) { case 0:
            var2 = 10; break; case 1:
            var2 = 20; break; case 2:
            var2 = 30; break; default:
            var2 = 40;
        } return var2;
    }
}

可以看得出,主要流程为:

  • 首先使用hashcode方法获取到字符串的哈希值
  • 因为case条件中的字符串的哈希值大概率不连续,所以字节码指令一般是使用lookupswitch来保存对应的偏移量
  • 之后执行偏移量的字节码指令,一上来会调用equals来判断var1是否与case条件中的String相匹配(因为可能存在两个不同字符串的哈希值相同的情况
  • 匹配成功后,会再利用tableswitch来查询var4代表的偏移量
  • 最后返回var2

总的来说,使用String类型来作为case中的条件,本质上还是先转化为hashcode,接着查到对应的hashcode,最后再利用equals检测内容是否相同。


第四个示例:使用枚举类型

public enum Animal {
        CAT, DOG
    } public int switchEnum(Animal animal) { int result; switch (animal) { case CAT:
                result = 1; break; case DOG:
                result = 2; break; default:
                result = 3;
        } return result;
    }

编译该文件会得到3个文件,分别是

  1. Main.class                     这个文件是肯定有的
  2. Main$Animal.class        这个是枚举类,也是正常的
  3. Main$1.class                  这是个啥?

进到Main$1.class发现:

import com.yang.testSwitch.Main.Animal; // $FF: synthetic class class Main$1 { static { try {
            $SwitchMap$com$yang$testSwitch$Main$Animal[Animal.CAT.ordinal()] = 1;
        } catch (NoSuchFieldError var2) {
        } try {
            $SwitchMap$com$yang$testSwitch$Main$Animal[Animal.DOG.ordinal()] = 2;
        } catch (NoSuchFieldError var1) {
        }

    }
}

使用javap -p Main$1.class后:

class com.yang.testSwitch.Main$1 { static final int[] $SwitchMap$com$yang$testSwitch$Main$Animal; static {};
}

原来这个class里,声明了一个静态的数组,数组利用枚举的ordinal()值作为下标,数组中的元素依次递增。

那这个数组到底是干啥用的呢?

我们接着使用javap -c -p Main.class反编译得到:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchEnum(com.yang.testSwitch.Main$Animal);
    Code:
       0: getstatic     #2                  // Field com/yang/testSwitch/Main$1.$SwitchMap$com$yang$testSwitch$Main$Animal:[I
       3: aload_1
       4: invokevirtual #3                  // Method com/yang/testSwitch/Main$Animal.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 41
               default: 46
          }
      36: iconst_1
      37: istore_2
      38: goto          48
      41: iconst_2
      42: istore_2
      43: goto          48
      46: iconst_3
      47: istore_2
      48: iload_2
      49: ireturn
}

现在可以发现,首先是获取到这个静态数组,再调用枚举的ordinal()方法获取枚举的值,再将这个值当作静态数组的下标,获取这个静态数组中的某一个元素,然后从lookupswitch中寻找偏移量。


第五个示例:使用包装类型

public int switchByte(Byte i) { int result; switch (i) { case 1:
                result = 10; break; case 2:
                result = 20; break; case 3:
                result = 30; break; default:
                result = 40;
        } return result;
    }

反编译得到:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchByte(java.lang.Byte);
    Code:
       0: aload_1
       1: invokevirtual #2                  // Method java/lang/Byte.byteValue:()B
       4: tableswitch   { // 1 to 3
                     1: 32
                     2: 38
                     3: 44
               default: 50
          }
      32: bipush        10
      34: istore_2
      35: goto          53
      38: bipush        20
      40: istore_2
      41: goto          53
      44: bipush        30
      46: istore_2
      47: goto          53
      50: bipush        40
      52: istore_2
      53: iload_2
      54: ireturn
}

可以很清晰的看到,调用了Byte.byteValue()方法完成了对Byte的拆箱工作,之后比较byte值即可。


总结

我们用一个表格,来直观的说明各种类型的底层操作:

         类型

                                                                    操作

       基本类型

直接比较(条件半连续,采用tableswitch,否则lookupswitch)

        String

首先获取hashcode,之后采用equals判断

        Enum

获取ordinal()值,集合一个自动生成的数组

      包装类型

先进行拆箱,之后比较基本类型

这里有几点需要注意的是:

(1)根据java虚拟机规范,java虚拟机的tableswitch和lookupswitch指令都只能支持int类型的条件值,所以switch不支持long、float、double与boolean,以及他们对应的包装类。

(2)break语句会在字节码层面生成一个goto语句,用来直接跳转出switch语句块。因此,如果有哪一次忘记写break,那么程序执行就会进入下一个case中,可能会造成某些匪夷所思的问题。




全部评论
更多文章请移步https://blog.csdn.net/qq_33591903
点赞 回复
分享
发布于 2020-04-09 15:01

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务