| 
 | 
 
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册  
 
x
 
转载自http://www.jianshu.com/p/9103ab536bbe; 
 
记得读大学教我们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."<init>":()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."<init>":()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."<init>":()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."<init>":()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体积也有那么点帮助。 
 
 |   
 
 
 
 |