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

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

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

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

    public static int test(int i) {
final int j = 2;
int result;
switch (i) {
case 0:
result = 0;
break;
case j:
result = 1;
break;
case 10:
result = 4;
break;
default:
result = -1;
}
return result;
}

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

public class SwitchTest {

    private final int caseJ;

    public int test(int i) {
int result;
switch (i) {
case 0:
result = 0;
break;
case caseJ:
result = 1;
break;
case 10:
result = 4;
break;
default:
result = -1;
}
return result;
} SwitchTest(int caseJ) {
this.caseJ = caseJ;
} public static void main(String[] args) {
SwitchTest testJ = new SwitchTest(1);
System.out.print(testJ.test(2));
}
}

lookupswitch和tableswitch

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

lookupswitch

    public static int test(int i) {

        int result;
switch (i) {
case 0:
result = 0;
break;
case 2:
result = 1;
break;
case 10:
result = 4;
break;
default:
result = -1;
}
return result;
}

对应字节码

  public static int test(int);
Code:
0: iload_0
1: lookupswitch { // 3
0: 36
2: 41
10: 46
default: 51
}
36: iconst_0
37: istore_1
38: goto 53
41: iconst_1
42: istore_1
43: goto 53
46: iconst_4
47: istore_1
48: goto 53
51: iconst_m1
52: istore_1
53: iload_1
54: ireturn

tableswitch

    public static int test(int i) {

        int result;
switch (i) {
case 0:
result = 0;
break;
case 2:
result = 1;
break;
case 4:
result = 4;
break;
default:
result = -1;
}
return result;
}
  public static int test(int);
Code:
0: iload_0
1: tableswitch { // 0 to 4
0: 36
1: 51
2: 41
3: 51
4: 46
default: 51
}
36: iconst_0
37: istore_1
38: goto 53
41: iconst_1
42: istore_1
43: goto 53
46: iconst_4
47: istore_1
48: goto 53
51: iconst_m1
52: istore_1
53: iload_1
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看到以下代码:

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

    public static int test2(int i) {

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

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

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

  1. case "ghi"分支里是没有赋值代码
  2. case "test"分支和case "test2"分支相同
    public static int testString(String str) {

        int result = -4;
switch (str) {
case "abc":
result = 0;
break;
case "def":
result = 1;
break;
case "ghi":
break;
case "test":
case "test2":
result = 1;
break;
default:
result = -1;
}
return result;
}

对应字节码

  public static int testString(java.lang.String);
Code:
0: bipush -4
2: istore_1
3: aload_0
4: astore_2
5: iconst_m1
6: istore_3
7: aload_2
8: invokevirtual #2 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 5
96354: 60
99333: 74
102312: 88
3556498: 102
110251488: 116
default: 127
}
60: aload_2
61: ldc #3 // String abc
63: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
66: ifeq 127
69: iconst_0
70: istore_3
71: goto 127
74: aload_2
75: ldc #5 // String def
77: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
80: ifeq 127
83: iconst_1
84: istore_3
85: goto 127
88: aload_2
89: ldc #6 // String ghi
91: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
94: ifeq 127
97: iconst_2
98: istore_3
99: goto 127
102: aload_2
103: ldc #7 // String test
105: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
108: ifeq 127
111: iconst_3
112: istore_3
113: goto 127
116: aload_2
117: ldc #8 // String test2
119: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
122: ifeq 127
125: iconst_4
126: istore_3
127: iload_3
128: tableswitch { // 0 to 4
0: 164
1: 169
2: 174
3: 177
4: 177
default: 182
}
164: iconst_0
165: istore_1
166: goto 184
169: iconst_1
170: istore_1
171: goto 184
174: goto 184
177: iconst_1
178: istore_1
179: goto 184
182: iconst_m1
183: istore_1
184: iload_1
185: ireturn

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

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

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

枚举的case表达式及字节码

样例代码如下

    public static int testEnum(StatusEnum statusEnum) {

        int result;
switch (statusEnum) {
case INIT:
result = 0;
break;
case FINISH:
result = 1;
break;
default:
result = -1;
}
return result;
}

对应字节码

  public static int testEnum(com.example.StatusEnum);
Code:
0: getstatic #9 // Field com/example/SwitchTest$1.$SwitchMap$com$example$core$service$domain$enums$StatusEnum:[I
3: aload_0
4: invokevirtual #10 // Method com/example/core/service/domain/enums/StatusEnum.ordinal:()I
7: iaload
8: lookupswitch { // 2
1: 36
2: 41
default: 46
}
36: iconst_0
37: istore_1
38: goto 48
41: iconst_1
42: istore_1
43: goto 48
46: iconst_m1
47: istore_1
48: iload_1
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. dotnetspider

    http://www.cnblogs.com/modestmt/p/5525467.html nuget :DotnetSpider2.Core

  2. SQL Server 限制IP登陆(登陆触发器运用)

    原文:SQL Server 限制IP登陆(登陆触发器运用) 一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 补 ...

  3. iostat命令浅析

    报告中央处理器(CPU)统计信息.整个系统.适配器.TTY 设备.磁盘 CD-ROM.磁带和文件系统的异步输入/输出(AIO)与输入/输出统计信息,iostat也有一个弱点,就是它不能对某个进程进行深 ...

  4. UWP开发-获取设备唯一ID

    EasClientDeviceInformation deviceInfo = new EasClientDeviceInformation(); this.showDeviceInfo.Items. ...

  5. Arch Linux 是个 针对 i686 优化的 Linux 发行版(通过可以轻松使用的二进制包系统 - pacman)

    Arch Linux 是个 针对 i686 优化的 Linux 发行版(通过可以轻松使用的二进制包系统 - pacman)Arch 同时也拥有一个类似 ports 的包构建系统(Arch Build ...

  6. Westciv Tools主要为CSS3提供了渐变gradients、盒子阴影box-shadow、变形transform和文字描边四种在线生成效果的工具

    Westciv Tools主要为CSS3提供了渐变gradients.盒子阴影box-shadow.变形transform和文字描边四种在线生成效果的工具 1.Westciv Tools 彩蛋爆料直击 ...

  7. 使用PyQt5编写一个简单的GUI程序(pyside 有 pyside-uic 把ui文件转成py文件,pyside-rcc 把qrc文件转成 py文件导入就行了)

    我做Python窗口界面编程时,经常使用PyQt进行设计.这里简单叙述一下使用PyQt5制作一个简单的图形界面的流程 PyQt的简介以及开发环境的搭建在此不多赘述. 1.       打开Qt Des ...

  8. Delphi下IOC 模式的实现(反转模式,即Callback模式)

    IOC英文为 Inversion of Control,即反转模式,这里有著名的好莱坞理论:你呆着别动,到时我会找你.Ioc模式是解决调用者和被调用者之间关系的模式,可以有效降低软件的耦合度,并适合团 ...

  9. springboot中加分布式redis锁

    分布式redis锁,spring-boot-starter-data-redis,RedisTemplate 公司聊天的聊天系统,近期出现多个客服并发接待同一个客户的记录,经排查,是由于代码加的同步锁 ...

  10. gitlab安装笔记三_Centos7安装GitLab

    系统版本是CentOS-7-x86_64-Everything-1804.iso,很多软件默认都有了,不需要安装 https://about.gitlab.com/install/#centos-7 ...