字节码角度分析面试题 —— i++、++i 傻傻分不清楚

一、什么都憋说,先看题

public class Test {
    public static void foo() {
        int i = 0;
        for (int j = 0; j < 50; j++) {
            i = i++;
        }
        System.out.println(i);
    }
    
    public static void main(String[] args) {
        foo();
    }
}

答案在末尾

二、i++

就拿一开始的题目作为栗子

javap 查看字节码

javap -c -v Test

复制 foo() 函数部分,如下

public static void foo();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iconst_0
         3: istore_1
         4: goto          15
         7: iload_0
         8: iinc          0, 1
        11: istore_0
        12: iinc          1, 1
        15: iload_1
        16: bipush        50
        18: if_icmplt     7
        21: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_0
        25: invokevirtual #21                 // Method java/io/PrintStream.println:(I)V
        28: return

========= 下面对应 int i = 0; 执行过程

  • 6 行:iconst_0 指令表示 int 类型的数值 0 放到操作数栈栈顶,这个数值 0 就是变量 i
  • 7 行:istore_0 指令表示将变量 i(初始值为 0)放到局部变量表下标为 0 的位置
    ========= 下面对应 int j = 0 执行过程
  • 8 行:iconst_0 指令表示 int 类型的数值 0 放到操作数栈栈顶,这个数值 0 就是变量 j
  • 9 行:istore_0 指令表示将变量 j(初始值为 0)放到局部变量表下标为 1 的位置
  • 10 行:goto 指令跳转到指令 15(第 15 行),iload_1 指令加载局部变量表下标为 1 的位置的 int 类型变量 j 到操作数栈栈顶
    ========= 下面对应 i = i++ 执行过程
  • 11 行:iload_0 指令将局部变量表下标为 0 的位置的变量 i 放到操作数栈栈顶
  • 12 行:iinc 0, 1 指令将局部变量表下标为 0 的位置的元素(当前为 0)加 1
  • 13 行:istore_0 指令将操作数栈栈顶元素的值存入局部变量表下标为 0 的位置(即把变量 i 放到局部变量表下标为 0 的位置,刚刚是 1,现在又变成 0 了)
    ========= 下面对应 for 循环的条件语句 j < 50; j++ 执行过程
  • 14 行:iinc 1, 1 指令将局部变量表下标为 1 的位置的变量 j 加 1
  • 15 行:iload_1 指令表示将局部变量表下标为 1 的位置的变量 j 放到操作数栈栈顶(现在的值在原来基础上加 1 了)
  • 16 行:bipush 50 指令表示将 int 类型 50 推送至栈顶
  • 17 行:if_icmplt 7 指令用于比较栈顶两int型数值大小,当结果小于0时跳转(即比较 j 和 50 的大小,当前肯定是 j < 50 为 true)到指令 7(11 行)
    ========= 下面对应 System.out.println(i); 执行过程
  • 18 行:getstatic 指令调用 System.out 静态属性,类型为 PrintStream
  • 19 行:iload_0 指令将局部变量表下标为 0 的位置的变量 i 放到操作数栈栈顶
  • 20 行:invokevirtual 指令调用 PrintStream.println() 方法
    ========= 下面对应 foo() 方法执行完了
  • 21 行:return 指令表示方法执行完了

整个过程,大致就是,获取 i、j 的值,然后先对 j 和 50 的大小进行比较,当 j < 50 为 true 时,对 i 进行 i++ 的操作,当 j < 50 为 false 时,跳出 for 循环,执行 System.out.println(i); 打印语句,然后 return,表示方法执行完了
其中,i++ 的操作对应三个步骤:
① 将局部变量表下标为 0 的位置的变量 i 放到操作数栈栈顶,此时变量 i 值为 0
② 将局部变量表下标为 0 的位置的值加 1,值默认为 0,加 1 后变为 1
③ 将操作数栈栈顶的变量 i(值为 0)放到局部变量表下标为 0 的位置,放之前下标为 0 的位置的值为 1,放之后值从 1 又变回 0

三、++i

public class Test {
    public static void foo() {
        int i = 0;
        for (int j = 0; j < 50; j++) {
            i = ++i;
        }
        System.out.println(i);
    }
    
    public static void main(String[] args) {
        foo();
    }
}

javap 查看字节码

javap -c -v Test

复制 foo() 函数部分,如下

public static void foo();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iconst_0
         3: istore_1
         4: goto          15
         7: iinc          0, 1
        10: iload_0
        11: istore_0
        12: iinc          1, 1
        15: iload_1
        16: bipush        50
        18: if_icmplt     7
        21: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_0
        25: invokevirtual #21                 // Method java/io/PrintStream.println:(I)V
        28: return

和 i++ 的字节码简单对比一下,i++ 和 ++i 两者生成的字节码中,会发现只有 11、12、13 这三行的指令顺序不一致,那就直接看下这三行到底有何不同

  • 11 行:iinc 0, 1 指令将局部变量表下标为 0 的位置的元素(当前为 0)加 1,变量 i 值为 1
  • 12 行:iload_0 指令将局部变量表下标为 0 的位置的变量 i 放到操作数栈栈顶,栈顶为变量 i,值为 1
  • 13 行:istore_0 指令将操作数栈栈顶元素变量 i 的值存入局部变量表下标为 0 的位置,值为 1

四、看一道 xue 微难一点的题目

public class Test {
    public static void bar() {
        int i = 0;
        i = i++ + ++i;
        System.out.println("i=" + i);
    }
    
    public static void main(String[] args) {
    	bar();
	}
}

javap 查看字节码

javap -c -v Test

复制 bar() 函数部分,如下

public static void bar();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: iinc          0, 1
         6: iinc          0, 1
         9: iload_0
        10: iadd
        11: istore_0
        12: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        15: new           #21                 // class java/lang/StringBuilder
        18: dup
        19: ldc           #23                 // String i=
        21: invokespecial #25                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        24: iload_0
        25: invokevirtual #28                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        28: invokevirtual #32                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: invokevirtual #36                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: return
  • 6 行:iconst_0 指令表示 int 类型的数值 0 放到操作数栈栈顶,这个数值 0 就是变量 i
  • 7 行:istore_0 指令表示将变量 i(初始值为 0)放到局部变量表下标为 0 的位置
  • 8 行:iload_0 指令表示将局部变量表下标为 0 的位置的变量 i 放到操作数栈栈顶,现在值为 0
  • 9 行:iinc 0, 1 指令表示将局部变量表下标为 0 的位置的元素值加 1,现在值为 1
  • 10 行:iinc 0, 1 指令表示局部变量表下标为 0 的位置的元素值加 1,现在值为 2
  • 11 行:iload_0 指令表示将局部变量表下标为 0 的位置的元素放到操作数栈栈顶,值为 2,现在栈中的元素有两个,栈顶是 2,栈顶下面的是 0
    ========= 下面对应 i = i++ + ++i; 执行过程
  • 12 行:iadd 指令表示栈顶两个元素的值相加,然后将结果重新入栈,2 + 0 = 2,2 重新入栈
  • 13 行:istore_0 指令表示将操作数栈栈顶元素的值存入局部变量表下标为 0 的位置,值为 2
    ========= 下面对应 System.out() 执行过程
  • 14 行:getstatic 指令调用 System.out 静态属性,类型为 PrintStream
    ========= 对应 “i=” + i 字符串拼接的过程
  • 15、16 行:new、dup 指令创建了 StringBuilder 对象
  • 17 行:ldc 将 String 类型字符串 “i=” 放到操作数栈栈顶
  • 18 行:invokespecial 指令调用 StringBuilder 对象的构造器函数
  • 19 行:iload_0 指令表示将局部变量表下标为 0 的元素放到操作数栈栈顶,值为 2
  • 20 行:invokespecial 指令调用 StringBuilder.append() 方法
  • 21 行;invokespecial 指令调用 StringBuilder.toString() 方法
    ========= 下面对应 PrintStream.println(); 执行过程
  • 22 行:invokespecial 指令调用 PrintStream.println() 方法
    ========= 下面对应 foo() 方法执行完了
  • 23 行:return 指令表示方法执行完了

可以看到 13 行对应 i++ 和 ++i 相加的过程,最终计算的结果是 2

五、小结

i++ 和 ++i 字节码区别
i++ 和 ++i 区别

  • ++i 是先对局部变量表下标为 0(slot = 0)的位置的元素加 1,然后再把加 1 后的值放到操作数栈栈顶,然后出栈又将值赋给了局部变量表,写回的值是最新的值
  • 不管是 i++ 还是 ++i,实际上 i 的值都加了 1,只是当 ++ 在前时,加 1 的值会被覆盖,可能是立即覆盖(如:i = 0;,打印输出 i++,i = 0),也可能是后续覆盖(如:i = 0;,i = i++ + ++i,i = 0 + 2 = 2,加起来的 2 覆盖了加法第一个参数的 0)
  • i++ 和 ++i 中,对 i 加 1 操作发生在局部变量表中
  • i++ 和 ++i 中,如果 int 类型 i 要参与其它操作,则需要先通过 iload 指令进入操作数栈,然后再开始操作

六、最后一题

public class Test {
    public static void foo() {
        int i = 0;
        i = ++i + i++ + i++ + i++;
        System.out.println("i=" + i);
    }
    
    public static void main(String[] args) {
        foo();
    }
}

七、回答

开头问题答案:0,为什么是 0,参考 ## i++
最后一题答案:7,为什么是 7
简单分析一下:++i = 1,i++ = 1(实际上后续对 i 操作时,i 的值为 2),i++ = 2(实际上后续对 i 操作时,i 的值为 3),i++=3
加起来,1 + 1 + 2 + 3 = 7
可以尝试使用 javap 查看对应字节码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值