条件分支语句的实现
条件分支语句的实现
Java语言提供的条件分支语句包含if语句、switch语句、三目运算符,这些条件语句是如何通过字节码实现的呢?
if语句
使用Java语言实现的if语句如代码清单3-38所示。
代码清单3-38 if语句
public int ifFunc(int type) { if (type == 1) { return 100; } else if (type == 2) { return 1000; } else { return 0; } }
使用javap命令输出ifFunc方法的字节码如下。
public int ifFunc(int); Code: 0: iload_1 1: iconst_1 2: if_icmpne 8 5: bipush 100 7: ireturn 8: iload_1 9: iconst_2 10: if_icmpne 17 13: sipush 1000 16: ireturn 17: iconst_0 18: ireturn
偏移量为0、1、2三条字节码指令完成第一个if语句的判断。iload_1将参数type的值放入操作数栈顶,由于是非静态方法,所示局部变量表索引为0的Slot存储的是this引用,因此局部变量表索引为1的Slot存储的才是方法的第一个参数。iconst_1指令将立即数1放入操作数栈顶。if_icmpne指令完成操作数栈顶两个整数的比较,该指令的操作码为0xA0,指令执行需要一个操作数,操作数是当前方法某条字节码指令的偏移量。当栈顶的两个int类型的元素不相等时,跳转到操作数指向的字节码指令。
if_icmpne字节码指令是判断两个值不相等才跳转,这与java代码刚好相反。在java代码中,if左右两个元素相等才执行if体内的代码,而编译后字节码指令按if与else if、else的编写顺序生成,当if左右两个元素相等时继续往下执行便是对应java语言中的if语句的代码块,因此字节码层面会看到相反的条件比较跳转。
偏移量为8、9、10的三条字节码指令也是完成比较跳转的操作,最后一个else从偏移量为17的字节码指令开始,如果else代码块中没有返回指令,那么会继续往下执行。如果第一个if中没有返回指令呢?如代码清单3-39所示。
代码清单3-39 if语句中没有return
public int ifFunc2(int type) { if (type == 1) { type = 2; }else { type = 3; } return type; }
使用javap命令输出ifFunc2方法的字节码如下。
public int ifFunc2(int); Code: 0: iload_1 1: iconst_1 2: if_icmpne 10 5: iconst_2 6: istore_1 7: goto 12 10: iconst_3 11: istore_1 12: iload_1 13: ireturn
如字节码所示,编译器在if_icmpne指令后面为局部变量type赋值后,使用一条goto指令跳转到else结束的后面的第一条字节码指令。
所以,当if或者else if的代码块中没有return指令时,编译器会为其添加一条goto指令用于跳出if条件分支语句。goto指令是无条件跳转指令,操作码为0xA7,操作数是当前方法的某条字节码指令的偏移量,本例中,goto指令的操作码是12,表示跳转到偏移量为12的字节码指令,偏移量为12的字节码指令是iload_1,所以goto指令之后将会指向该指令。
if_icmpne指令用于两个int类型值比较,不相等才跳转,更多比较跳转指令如表3-40所示。
表3-40 条件比较跳转指令


与0比较的跳转指令如表3-41所示。
表3-41 与0比较的条件跳转指令


switch语句
使用Java语言实现的switch语句如代码清单3-42所示。
代码清单3-42 紧凑的switch
public int switchFunc(int stat) { int a = 0; switch (stat) { case 5: a = 0; break; case 6: case 8: a = 1; break; } return a; }
使用javap命令输出switchFunc方法的字节码如下。
public int switchFunc(int); Code: 0: iconst_0 1: istore_2 2: iload_1 3: tableswitch { // 5 to 8 5: 32 6: 37 7: 39 8: 37 default: 39 } 32: iconst_0 33: istore_2 34: goto 39 37: iconst_1 38: istore_2 39: iload_2 40: ireturn
与if语句一样的是,switch代码块中的每个case代码块都是按顺序编译生成字节码的,switch代码块中的所有字节码都在tableswitch这条指令的后面。
tableswitch指令的操作码为0xAA,该指令的操作数是不定长的,每个操作数的长度为四个字节,编译器会为case区间(本例中,case最小值为5,最大值为8,区间为[5,8])的每一个数字都生成一个case语句,就是添加一个操作数,操作数存放下一条字节码指令的相对偏移量,注意,是相对偏移量。以上面例子说明,tableswitch指令对应的字节码为:
AA | 00 00 00 24 | 00 00 00 05 | 00 00 00 08 | 00 00 00 1D | 00 00 00 22 | 00 00 00 24 | 00 00 00 22
第一个字节0xAA是tableswitch指令的操作码,后面每四个字节为一个操作数。前面四个字节0x00000024转为10进制是36,由于tableswitch指令的偏移量为3,因此该操作数表示匹配default时跳转到偏移量为39的字节码指令。紧随其后的是0x00000005与0x00000008,这两个数代表表格的区间,从5到8,也就是case 5到case 8,虽然我们代码中没有case 7,编译器还是为我们生成了。后面的0x0000001d、0x00000022、0x00000024、0x00000022分别+3得到的结果就是case 5到8分别跳转到的目标字节码指令的绝对偏移量。
从前面的例子我们可以看出,tableswitch指令生成的字节码占用的空间很大,而且当case的值不连续时,还会生成一些无用的映射。如果case的每个值都不连续呢?如代码清单3-43。
代码清单3-43 非紧凑的switch
public int switch2Func(int stat) { int a = 0; switch (stat) { case 1: a = 0; break; case 100: a = 1; break; } return a; }
假设,编译器将代码清单3-43的switch语句生成tableswitch指令,那么这条指令将浪费掉4乘以98的字节空间,如果再加个case 1000,那么浪费的空间更大。显然,这种情况下再使用tableswitch指令是不可取的。
使用javap输出代码清单3-43 switch2Func方法的字节码如下。
public int switch2Func(int); Code: 0: iconst_0 1: istore_2 2: iload_1 3: lookupswitch { // 2 1: 28 100: 33 default: 35 } 28: iconst_0 29: istore_2 30: goto 35 33: iconst_1 34: istore_2 35: iload_2 36: ireturn
正如你所看到的,编译器使用lookupswitch指令替代了tableswitch指令。lookupswitch指令的操作码为0xAB,与tableswitch指令一样,该指令的操作数也是不定长的,每个操作数的长度为四个字节,操作数存放的也是下一条字节码指令的相对偏移量,注意,还是相对偏移量。以上面例子说明,lookupswitch指令对应的字节码为。
AB | 00 00 00 20 | 00 00 00 02 | 00 00 00 01 00 00 00 19 | 00 00 00 64 | 00 00 00 1E
第一个字节0xAB是lookupswitch指令的操作码,接着后面四个字节也是匹配default时跳转的目标指令相对当前指令的偏移量,紧随其后四个字节0x00000002代表后面跟随多少个条件映射,每八个字节为一个条件映射,前四个字节为匹配条件,后四个字节为条件匹配时跳转的目标字节码指令的相对偏移量。0x00000001表示当当前操作数栈栈顶的值为1时,跳转到相对偏移量为0x00000019的字节码指令,0x00000019转为10进制是25,加上当前lookupswitch指令的偏移量3等于28;0x00000064转为十进制为100,0x0000001E转为十进制加上3等于33。
三目运算符
三目运算符也叫三元运算符,这是由三个操作数组成的运算符。如代码清单3-44所示。
代码清单3-44 三目运算符
public int syFunc(boolean sex) { return sex ? 1 : 0; }
使用javap命令输出syFunc方法的字节码如下。
public int syFunc(boolean); Code: 0: iload_1 1: ifeq 8 4: iconst_1 5: goto 9 8: iconst_0 9: ireturn
由于方法参数sex是boolean类型,因此使用sex作为条件表达式编译后会使用ifeq指令实现跳转,即与0比较。当前操作数栈顶元素的值等于0则跳转,不等于0继续往下执行。
三目运算符的表达式为:<表达式1>?<表达式2>:<表达式3>。因此三目运算符也支持多层嵌套,但实际开发中不建议这么做,因为会导致代码能以理解。