关于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."<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体积也有那么点帮助。

写在最后

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容