摘要:本篇文章探讨了JAVA从入门到精通之深入理解Java的switch...case...语句,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入。
本篇文章探讨了JAVA从入门到精通之深入理解Java的switch...case...语句,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入。
"
最早时,只支持int、char、byte、short这样的整型的基本类型或对应的包装类型Integer、Character、Byte、Short常量
JDK1.5开始支持enum,原理是给枚举值进行了内部的编号,进行编号和枚举值的映射
1.7开始支持String,但不允许为null。(原因可以看后文)
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)); } }
下面两种几乎一样的代码,会编译出大相径庭的字节码。
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
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。
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个数量级的差距。
在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。此处可以看做是近似计算。
一般来说,更多的限制能带来更好的性能。
从上文可以看出,无论是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 ""ghi""分支里是没有赋值代码
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
可以看到与整型常量的不同:
String常量判等,先计算hashCode,在lookupswitch分支中再比较是否真正相等。这也是不支持null的原因,此时hashCode无法计算。
lookupswitch分支中,会给每个分支分配一个新下标值,作为后面的tableswitch的索引。源码中的分支语句统一在tableswitch中对应分支执行。
为什么要再生成一段tableswitch?从字节码来看,两个平行的分支(""test""和""test2""),虽然没有在tableswitch中用同一个数组下标,但是使用了同一个跳转行177,在这种情况下减少了字节码冗余。
样例代码如下
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,那么对应的字节码就会“滑落”到下一行语句,继续执行。
Mac下preference
->Tools
->External Tools
,点击+
,按如下页面配置即可。
Windows下需要将上图填入的javap改为javap.exe。
注意:每次查看字节码前,要确保对应类被重新编译,才能看到最新版。
这种情况的真实原因是,JDK设置不一致,IDE没有完全使用预期的编译器版本。
在IDEA里可以这样解决:Project Settings
-> Project
设置项目语言
如果仍未解决,检查File
-> Project Structure
-> Modules
, 查看所有模块是否都是预期的等级。
还有一处也可以看下File
-> Settings
-> Compiler
-> Java Compiler
. 这里可以设置项目及模块的编译器版本。
" 本文由职坐标整理发布,学习更多的相关知识,请关注职坐标IT知识库!
您输入的评论内容中包含违禁敏感词
我知道了
请输入正确的手机号码
请输入正确的验证码
您今天的短信下发次数太多了,明天再试试吧!
我们会在第一时间安排职业规划师联系您!
您也可以联系我们的职业规划师咨询:
版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
沪公网安备 31011502005948号