字节码角度分析面试题 —— 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 是先对局部变量表下标为 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 查看对应字节码