条件分支语句的实现

条件分支语句的实现

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>。因此三目运算符也支持多层嵌套,但实际开发中不建议这么做,因为会导致代码能以理解。