switch...case...中条件表达式的演进

  • 最早时,只支持int、char、byte、short这样的整型的基本类型或对应的包装类型Integer、Character、Byte、Short常量
  • JDK1.5开始支持enum,原理是给枚举值进行了内部的编号,进行编号和枚举值的映射
  • 1.7开始支持String,但不允许为null。(原因可以看后文)

case表达式仅限字面值常量吗?

case表达式既可以用字面值常量,也可以用final修饰且初始化过的变量。例如以下代码可正常编译并执行:

  1. public static int test(int i) {
  2. final int j = 2;
  3. int result;
  4. switch (i) {
  5. case 0:
  6. result = 0;
  7. break;
  8. case j:
  9. result = 1;
  10. break;
  11. case 10:
  12. result = 4;
  13. break;
  14. default:
  15. result = -1;
  16. }
  17. return result;
  18. }

但是没有初始化就不行,比如下面的代码就无法通过编译

  1. public class SwitchTest {
  2. private final int caseJ;
  3. public int test(int i) {
  4. int result;
  5. switch (i) {
  6. case 0:
  7. result = 0;
  8. break;
  9. case caseJ:
  10. result = 1;
  11. break;
  12. case 10:
  13. result = 4;
  14. break;
  15. default:
  16. result = -1;
  17. }
  18. return result;
  19. }
  20. SwitchTest(int caseJ) {
  21. this.caseJ = caseJ;
  22. }
  23. public static void main(String[] args) {
  24. SwitchTest testJ = new SwitchTest(1);
  25. System.out.print(testJ.test(2));
  26. }
  27. }

lookupswitch和tableswitch

下面两种几乎一样的代码,会编译出大相径庭的字节码。

lookupswitch

  1. public static int test(int i) {
  2. int result;
  3. switch (i) {
  4. case 0:
  5. result = 0;
  6. break;
  7. case 2:
  8. result = 1;
  9. break;
  10. case 10:
  11. result = 4;
  12. break;
  13. default:
  14. result = -1;
  15. }
  16. return result;
  17. }

对应字节码

  1. public static int test(int);
  2. Code:
  3. 0: iload_0
  4. 1: lookupswitch { // 3
  5. 0: 36
  6. 2: 41
  7. 10: 46
  8. default: 51
  9. }
  10. 36: iconst_0
  11. 37: istore_1
  12. 38: goto 53
  13. 41: iconst_1
  14. 42: istore_1
  15. 43: goto 53
  16. 46: iconst_4
  17. 47: istore_1
  18. 48: goto 53
  19. 51: iconst_m1
  20. 52: istore_1
  21. 53: iload_1
  22. 54: ireturn

tableswitch

  1. public static int test(int i) {
  2. int result;
  3. switch (i) {
  4. case 0:
  5. result = 0;
  6. break;
  7. case 2:
  8. result = 1;
  9. break;
  10. case 4:
  11. result = 4;
  12. break;
  13. default:
  14. result = -1;
  15. }
  16. return result;
  17. }
  1. public static int test(int);
  2. Code:
  3. 0: iload_0
  4. 1: tableswitch { // 0 to 4
  5. 0: 36
  6. 1: 51
  7. 2: 41
  8. 3: 51
  9. 4: 46
  10. default: 51
  11. }
  12. 36: iconst_0
  13. 37: istore_1
  14. 38: goto 53
  15. 41: iconst_1
  16. 42: istore_1
  17. 43: goto 53
  18. 46: iconst_4
  19. 47: istore_1
  20. 48: goto 53
  21. 51: iconst_m1
  22. 52: istore_1
  23. 53: iload_1
  24. 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看到以下代码:

  1. long table_space_cost = 4 + ((long) hi - lo + 1); // words
  2. long table_time_cost = 3; // comparisons
  3. long lookup_space_cost = 3 + 2 * (long) nlabels;
  4. long lookup_time_cost = nlabels;
  5. int opcode =
  6. nlabels > 0 && table_space_cost + 3 * table_time_cost <= lookup_space_cost + 3 * lookup_time_cost
  7. ?
  8. 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...是没有的,可以看下面的源码和字节码。

  1. public static int test2(int i) {
  2. int result;
  3. if(i == 0) {
  4. result = 0;
  5. } else if(i == 1) {
  6. result = 1;
  7. } else if(i == 4) {
  8. result = 4;
  9. } else {
  10. result = -1;
  11. }
  12. return result;
  13. }
  1. public static int test2(int);
  2. Code:
  3. 0: iload_0
  4. 1: ifne 9
  5. 4: iconst_0
  6. 5: istore_1
  7. 6: goto 31
  8. 9: iload_0
  9. 10: iconst_1
  10. 11: if_icmpne 19
  11. 14: iconst_1
  12. 15: istore_1
  13. 16: goto 31
  14. 19: iload_0
  15. 20: iconst_4
  16. 21: if_icmpne 29
  17. 24: iconst_4
  18. 25: istore_1
  19. 26: goto 31
  20. 29: iconst_m1
  21. 30: istore_1
  22. 31: iload_1
  23. 32: ireturn

字符串常量的case表达式及字节码

举例如下,这段源码有两个特点:

  1. case "ghi"分支里是没有赋值代码
  2. case "test"分支和case "test2"分支相同
  1. public static int testString(String str) {
  2. int result = -4;
  3. switch (str) {
  4. case "abc":
  5. result = 0;
  6. break;
  7. case "def":
  8. result = 1;
  9. break;
  10. case "ghi":
  11. break;
  12. case "test":
  13. case "test2":
  14. result = 1;
  15. break;
  16. default:
  17. result = -1;
  18. }
  19. return result;
  20. }

对应字节码

  1. public static int testString(java.lang.String);
  2. Code:
  3. 0: bipush -4
  4. 2: istore_1
  5. 3: aload_0
  6. 4: astore_2
  7. 5: iconst_m1
  8. 6: istore_3
  9. 7: aload_2
  10. 8: invokevirtual #2 // Method java/lang/String.hashCode:()I
  11. 11: lookupswitch { // 5
  12. 96354: 60
  13. 99333: 74
  14. 102312: 88
  15. 3556498: 102
  16. 110251488: 116
  17. default: 127
  18. }
  19. 60: aload_2
  20. 61: ldc #3 // String abc
  21. 63: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  22. 66: ifeq 127
  23. 69: iconst_0
  24. 70: istore_3
  25. 71: goto 127
  26. 74: aload_2
  27. 75: ldc #5 // String def
  28. 77: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  29. 80: ifeq 127
  30. 83: iconst_1
  31. 84: istore_3
  32. 85: goto 127
  33. 88: aload_2
  34. 89: ldc #6 // String ghi
  35. 91: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  36. 94: ifeq 127
  37. 97: iconst_2
  38. 98: istore_3
  39. 99: goto 127
  40. 102: aload_2
  41. 103: ldc #7 // String test
  42. 105: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  43. 108: ifeq 127
  44. 111: iconst_3
  45. 112: istore_3
  46. 113: goto 127
  47. 116: aload_2
  48. 117: ldc #8 // String test2
  49. 119: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  50. 122: ifeq 127
  51. 125: iconst_4
  52. 126: istore_3
  53. 127: iload_3
  54. 128: tableswitch { // 0 to 4
  55. 0: 164
  56. 1: 169
  57. 2: 174
  58. 3: 177
  59. 4: 177
  60. default: 182
  61. }
  62. 164: iconst_0
  63. 165: istore_1
  64. 166: goto 184
  65. 169: iconst_1
  66. 170: istore_1
  67. 171: goto 184
  68. 174: goto 184
  69. 177: iconst_1
  70. 178: istore_1
  71. 179: goto 184
  72. 182: iconst_m1
  73. 183: istore_1
  74. 184: iload_1
  75. 185: ireturn

可以看到与整型常量的不同:

  1. String常量判等,先计算hashCode,在lookupswitch分支中再比较是否真正相等。这也是不支持null的原因,此时hashCode无法计算。
  2. lookupswitch分支中,会给每个分支分配一个新下标值,作为后面的tableswitch的索引。源码中的分支语句统一在tableswitch中对应分支执行。

为什么要再生成一段tableswitch?从字节码来看,两个平行的分支("test"和"test2"),虽然没有在tableswitch中用同一个数组下标,但是使用了同一个跳转行177,在这种情况下减少了字节码冗余。

枚举的case表达式及字节码

样例代码如下

  1. public static int testEnum(StatusEnum statusEnum) {
  2. int result;
  3. switch (statusEnum) {
  4. case INIT:
  5. result = 0;
  6. break;
  7. case FINISH:
  8. result = 1;
  9. break;
  10. default:
  11. result = -1;
  12. }
  13. return result;
  14. }

对应字节码

  1. public static int testEnum(com.example.StatusEnum);
  2. Code:
  3. 0: getstatic #9 // Field com/example/SwitchTest$1.$SwitchMap$com$example$core$service$domain$enums$StatusEnum:[I
  4. 3: aload_0
  5. 4: invokevirtual #10 // Method com/example/core/service/domain/enums/StatusEnum.ordinal:()I
  6. 7: iaload
  7. 8: lookupswitch { // 2
  8. 1: 36
  9. 2: 41
  10. default: 46
  11. }
  12. 36: iconst_0
  13. 37: istore_1
  14. 38: goto 48
  15. 41: iconst_1
  16. 42: istore_1
  17. 43: goto 48
  18. 46: iconst_m1
  19. 47: istore_1
  20. 48: iload_1
  21. 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类字节码

深入理解Java的switch...case...语句的更多相关文章

  1. java中的Switch case语句

    java中的Switch case 语句 在Switch语句中有4个关键字:switch,case break,default. 在switch(变量),变量只能是整型或者字符型,程序先读出这个变量的 ...

  2. Java基础之循环语句、条件语句、switch case 语句

    Java 循环结构 - for, while 及 do...while 顺序结构的程序语句只能被执行一次.如果您想要同样的操作执行多次,,就需要使用循环结构. Java中有三种主要的循环结构: whi ...

  3. JavaSE基础(七)--Java流程控制语句之switch case 语句

    Java switch case 语句 switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支. 语法 switch case 语句语法格式如下: switch(exp ...

  4. Java switch case 语句

    switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支. 语法 switch(expression){ case value : //语句 break; //可选 ca ...

  5. Java switch case语句

    switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支. switch case 语句语法格式如下: switch(expression){ case value : ...

  6. c语言中的switch case语句

    switch--case语句中,switch后面跟一个变量,这个变量不可以是字符数组,字符指针,字符串数组,浮点型(实型).它可以是整型,字符型(在本质上也是整型).所以这导致case后面的常量表达式 ...

  7. 为什么说在使用多条件判断时switch case语句比if语句效率高?

    在学习JavaScript中的if控制语句和switch控制语句的时候,提到了使用多条件判断时switch case语句比if语句效率高,但是身为小白的我并没有在代码中看出有什么不同.去度娘找了半个小 ...

  8. JAVA基础——Switch条件语句

    JAVA基础——switch 条件语句 switch语句结构: switch(表达式){ case值1: 语句体1: break: case值2: 语句体2: break: case值3: 语句体3: ...

  9. 逆向知识第九讲,switch case语句在汇编中表达的方式

    一丶Switch Case语句在汇编中的第一种表达方式 (引导性跳转表) 第一种表达方式生成条件: case 个数偏少,那么汇编中将会生成引导性的跳转表,会做出 if else的情况(类似,但还是能分 ...

随机推荐

  1. 修改Hosts不生效的一个场景-web 专题

    准备工作 1.在 QQ互联 申请成为开发者,并创建应用,得到APP ID 和 APP Key.2.了解QQ登录时的 网站应用接入流程.(必须看完看懂) 为了方便各位测试,直接把我自己申请的贡献出来:A ...

  2. SICP 关于递归迭代的重新理解以及尾递归的引入...

    看了线性的递归和迭代以及树形递归迭代这部分的内容,感觉对递归和迭代又有了新的理解...所以记录一下,也算对这部分内容的总结吧. 首先书中提到的递归与迭代和我以前想的有点不一样,我感觉书中提到的递归和迭 ...

  3. 在 Excel 中如何使用宏示例删除列表中的重复项

    概要:在 Microsoft Excel 中,可以创建宏来删除列表中的重复项.也可以创建宏来比较两个列表,并删除第二个列表中那些也出现在第一个(主)列表中的项目.如果您想将两个列表合并在一起,或者如果 ...

  4. 【摘抄】C# DateTime.Now详解

    //2008年4月24日 System.DateTime.Now.ToString("D"); //2008-4-24 System.DateTime.Now.ToString(& ...

  5. 【Git】文件暂存与提交

    git工作目录文件的两种状态:已跟踪.未跟踪. 文件状态的变化周期: 查看当前文件状态: git status 跟踪新文件/暂存已修改文件 git add newfile 状态简览 git statu ...

  6. Win8Metro(C#)数字图像处理--2.5图像亮度调整

    原文:Win8Metro(C#)数字图像处理--2.5图像亮度调整  2.5图像亮度调整函数 [函数名称] 图像亮度调整函数BrightnessAdjustProcess(WriteableBit ...

  7. 微信小程序把玩(三十五)Video API

    原文:微信小程序把玩(三十五)Video API 电脑端不能测试拍摄功能只能测试选择视频功能,好像只支持mp4格式,值得注意的是成功之后返回的临时文件路径是个列表tempFilePaths而不是tem ...

  8. UI设计师必收!同行总结可即刻上手的iOS规范参考

    分享 <关于我> 分享  [中文纪录片]互联网时代                 http://pan.baidu.com/s/1qWkJfcS 分享 <HTML开发MacOSAp ...

  9. QPixmap的缓冲区

    我想qt 中QPixmap这个类大家都很熟悉,它可以很简单的在标签上贴图:例如: QPixmap p; p.load("1.png"): label->setPixmap(p ...

  10. 创建服务消费者(Feign)

    概述 Feign 是一个声明式的伪 Http 客户端,它使得写 Http 客户端变得更简单.使用 Feign,只需要创建一个接口并注解.它具有可插拔的注解特性,可使用 Feign 注解和 JAX-RS ...