关于Java中switch关键字你需要知道这些

阅读完本文我相信大家会有不少收货的,如果遇到不懂的地方,请耐心查阅相关知识。

JDK7之后switch为什么可以支持String类型的条件判断

记得读大学教我们Java课程的老师曾说,switch判断条件的数据类型只支持int和char。但是现在看来,这句话就不是那么严谨了,因为JDK7之后,还支持String类型的判断条件。接下来分析一步步分析其中的原理。

示例代码

public class TestSwitch {
    public static final java.lang.String CASE_ONE = "1";

    public static final java.lang.String CASE_TWO = "2";

    public static final java.lang.String CASE_THERE = "3";

    public static final java.lang.String CASE_FOUR = "4";

    public void testSwitch(String key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_THERE:
            break;
        case CASE_FOUR:
            break;
        default:
            break;
        }
    }
}

接着通过以下命令,将该Java文件转成Class文件

javac TestSwtich.java

然后通过以下命令,将编译后的Class文件进行反编译

javap TesTSwitch.class

(注意:以上javac和javap命令是JDK工具提供的,如有不了解的可以通过javac -help和javap -help进行了解)

得到如下汇编代码。

public class TestSwitch {
  public static final java.lang.String CASE_ONE;

  public static final java.lang.String CASE_TWO;

  public static final java.lang.String CASE_THERE;

  public static final java.lang.String CASE_FOUR;

  public TestSwitch();

    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return
  //这里开始是分析的重点
  public void testSwitch(java.lang.String);

    Code:
//将方法参数key加载进操作数栈
       0: aload_1
//接着将该方法参数key存储到局部变量表
       1: astore_2
//将int常量-1压入操作数栈中
       2: iconst_m1
//接着将刚压入栈中的常量-1存储到局部变量表中
       3: istore_3
//将局部变量表中的存储的方法参数key加载到操作数栈顶
       4: aload_2
//这一步是关键,接着虚拟机会调用String的hashCode方法,
//即对示例源码中key值进行hashCode,这样做的目的就是转成对int类型的判断
       5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
//taleswitch是Java虚拟机对应Java源码中switch关键字的指令                                 
       8: tableswitch   { // 49 to 52       
//49对应着常量字符串“1”的hashCode值,也是字符‘1’的ASCII值,
//大家可以看下String类hasCode的源码就会知道为什么相等了。
//如果字符串key的hashCode值等于常量字符串“1”的hashCode值,
//则跳转到行号为40的地方继续执行指令
                    49: 40                  
                    50: 54
                    51: 68
                    52: 82
               default: 93
          }
//将局部变量表中的存储的方法参数key加载到操作数栈顶        
      40: aload_2                          
// 将常量池中的常量字符串“1”压入栈中
      41: ldc           #3                
//接着调用String的equals方法将常量字符串“1”和key进行比较,接着讲返回值压入栈顶
//虽然equals方法的返回值是布尔类型,但是Java虚拟机会将布尔类型窄化成int型。
      43: invokevirtual #4   // Method java/lang/String.equals:(Ljava/lang/Object;)Z
                                           
 //从栈顶中弹出int型数据,如果为0则跳转到行号为93的地方进行执行,0代表false
      46: ifeq          93                
  //将int型常量0压入栈中 
//为什么会把0压入栈中?因为虚拟机会将第一个case情况默认赋值为0,后面的case情况依次+1
      49: iconst_0
  //将int型常量0存储到局部变量表中                        
      50: istore_3              
  //接着跳转到行号为93的地方继续执行         
      51: goto          93               
      54: aload_2
      55: ldc           #5                  // String 2
      57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      60: ifeq          93
      63: iconst_1
      64: istore_3
      65: goto          93
      68: aload_2
      69: ldc           #6                  // String 3
      71: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      74: ifeq          93
      77: iconst_2
      78: istore_3
      79: goto          93
      82: aload_2
      83: ldc           #7                  // String 4
      85: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      88: ifeq          93
      91: iconst_3
      92: istore_3
//以上每种case情况,最终会跳转到该行指令执行
//上面分析中,如果case情况为字符串“1”,则会保存一个int常量0,
//这里0也就代表了case为“1”的情况。
//这句指令会把先前存储在局部变量表中的int值加载到栈顶
      93: iload_3                          
      94: tableswitch   { // 0 to 3         
//如果等于0,则跳转到124行指令处执行
                     0: 124                
                     1: 127
                     2: 130
                     3: 133
               default: 136
          }
     124: goto          136
     127: goto          136
     130: goto          136
     133: goto          136
     136: return
}

以上代码中,我分析了"case CASE_ONE:"情况,虽然我们Java代码中只用了一个switch关键字,但是编译器生成的字节码却用了两个tableswitch指令。第一条tableswitch指令是根据字符串key哈希之后的int值进行调整判断,跳转到相应的行号之后,接着调用equals方法进行字符串内容比较,如果内容相等,会将每种case情况用一个int值记录,从0开始依次加1。第二条tableswitch指令会根据每种case情况所对应的int值进行判断,最终转化为switch的判断条件为int类型的情况。
由此可见,用String类型作为判断条件,编译器编译后的指令也会相应的增加。因此建议,能够用int值作为判断条件的就用int值吧。

如果对Java虚拟机指令不了解的,请耐心翻阅相关书籍或查阅相关资料。我相信你也会有不少收货的。

如何写出高效的switch代码

看到这个标题,不要惊讶。我们边写示例边分析原理。

示例代码1

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_THERE = 3;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_THERE:
            break;
        default:
            break;
        }
    }
}

反编译后得到:

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_THERE;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: tableswitch   { // 1 to 3
                     1: 28
                     2: 31
                     3: 34
               default: 37
          }
      28: goto          37
      31: goto          37
      34: goto          37
      37: return
}

示例代码2

“示例代码2”在“示例代码1”的基础上,仅仅修改了最后一个case情况的判断常量数的值。从“3”变成“5"。

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_FIVE = 5;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_FIVE:
            break;
        default:
            break;
        }
    }
}

反编译后得到

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_FIVE;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: tableswitch   { // 1 to 5
//“示例代码2”相对“示例代码1”来说仅仅改变了最后一个case的判断常量数的值。
//但是会导致所有case判断常量数的值不连续了。“示例代码1”是“1,2,3”,这个三个数是连续的。
//但是本示例中变成了“1,2,5”,这个三个数就不连续了。tableswitch这个指令比较“聪明”的。
//如果判断数值是不连续的,且又不是那么离散,那么它会自动把中间缺的判断常量数给补上。
//例如下面代码,判断条件3和4是编译器帮我们补上的。
//补上后有什么好处呢?补上后,“1,2,3,4,5”这些判断条件值就是一个连续值的整型数组,利于判断的直接跳转。
//比如我们传入的判断条件数值是3,即switch(key)中key值为3,
//那么虚拟机会首先判断3是否在1-5之间,
//如果在则取目标值1(即下面“1:36”的这行代码)为参照,
//接着直接跳转到数组中(3-1)项(注意:数组是从0开始),即“3:45”代码出。
                     1: 36
                     2: 39
                     3: 45
                     4: 45
                     5: 42
               default: 45
          }
      36: goto          45
      39: goto          45
      42: goto          45
      45: return
}

示例代码3

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_TEN = 10;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_TEN:
            break;
        default:
            break;
        }
    }
}

反编译后得到

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_TEN;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: lookupswitch  { // 3
//这里我们将最后一个判断条件的数值改为10后,
//这里出现的不是tableswitch指令,而是lookupswitch指令了。
//原因是,case中所有的判断条件的数值比较离散了,如果系统继续帮我们补其剩余判断数的话(即从3到9),
//那么会浪费不少内存空间。因此这里改用lookupswitch指令,那么该指令是如何执行呢?
//其实很简单就是一步一步的比较下去,知道碰到条件满足的。
                     1: 36
                     2: 39
                    10: 42
               default: 45
          }
      36: goto          45
      39: goto          45
      42: goto          45
      45: return
}

这三种情况我只改变了最后一个case的判断常量数的值,但是得到的反编译代码却有所不同。
对于编译器来说,switch的case判断条件值,有三种情况。
1.判断值都是连续数字。
2.判断数字不连续但也不很离散。
3.判断数字比较离散。
以上第一种和第二种情况是使用tableswitch指令,第三种情况使用lookupswitch指令。
这里大家可能会对第二种情况有些许疑问,就是如何判断不连续但也不很离散,这里虚拟机有一套判断规则,会根据switch语句的case个数和case的所有判断常量数值的离散情况而定。
简而言之,tableswitch指令是以空间换时间来提供效率,而lookupswitch会“牺牲”效率来换取空间。但是我们不需要考虑这些,因为聪明的编译器会帮我们搞定。

使用switch关键字的建议。

switch是我们经常打交道的关键字,但是在写判断条件的时候我们不应该很随意设置。
比如

public class TestSwitch {

    public static final int DO_SWIM = 1;

    public static final int DO_EAT = 2;

    public static final int DO_DRINK = 3;
        ……
    public void testSwitch(int key) {
        switch (key) {
        case DO_SWIM:
            break;
        case DO_EAT:
            break;
        case DO_DRINK:
            break;
                ……
        default:
            break;
        }
    }
}

DO_SWIM、DO_EAT、DO_DRINK……,我们分别代表三种不用行为,如果分别设置为1、2、3……这种连续数据。那么这种代码就是相当完美的。但是有时候有些程序员比较“另类”,可能会设成1,3,5,7……这种纯"奇"数据。或者10,100,1000,10000……这种霸气数据。后两种情况的数据要么是会造成空间浪费,要么是会影响执行效率。因此,我们在写switch指令时,如果情况允许,最好将case的判断数据设置为连续的,同时对减少apk体积也有那么点帮助。

写在最后

小小的代码背后都可能会蕴含高深的原理,保持一颗求知的心,我们才能不断进步,共勉之!当然,如果文章有何错误或写的不明白之处,欢迎大家指出,非常感谢阅读!

你可能感兴趣的:(关于Java中switch关键字你需要知道这些)