初学编程时,i++ 与 ++i 经常搞错,哪怕是在工作多年后,也只是知道 ++i 是先自增运算赋值再进行操作,i++ 是先进行操作,然后再进行自增运算赋值,只能说是知其然,却不知其所以然,作为研究 JVM 第一站,先剖析一下这个经典的问题。
首先,先列出今天的测试代码:
/**
* 从字节码层面分析 i++ 和 ++i 为什么不同
*/
public class AnalyseBytecode {
public static void main(String[] args) {
int i = 0;
int num = i++;
System.out.println("num: " + num);
num = ++i;
System.out.println("num: " + num);
}
}
代码很简单,就不过多介绍了。
其次,执行 javac -g:vars 具体文件路径/AnalyseBytecode .java 将 java 文件编译成 class 文件。
注意,-g:vars 一定要加上,要不然 LocalVariableTable 不会出现。
然后,通过执行 javap -v 文件路径/AnalyseBytecode.class > 指定路径/AnalyseBytecode.txt 实现反汇编,就可以进入正题了。
Classfile ~/AnalyseBytecode.class
Last modified 2021-1-5; size 752 bytes
MD5 checksum 06e2e2dd5a25824e2d4033df5539f693
public class com.wd.clazz.AnalyseBytecode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #28 // java/lang/StringBuilder
#4 = Methodref #3.#25 // java/lang/StringBuilder."<init>":()V
#5 = String #29 // num:
#6 = Methodref #3.#30 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #3.#31 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#8 = Methodref #3.#32 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Class #35 // com/wd/clazz/AnalyseBytecode
#11 = Class #36 // java/lang/Object
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/wd/clazz/AnalyseBytecode;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 i
#23 = Utf8 I
#24 = Utf8 num
#25 = NameAndType #12:#13 // "<init>":()V
#26 = Class #37 // java/lang/System
#27 = NameAndType #38:#39 // out:Ljava/io/PrintStream;
#28 = Utf8 java/lang/StringBuilder
#29 = Utf8 num:
#30 = NameAndType #40:#41 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#31 = NameAndType #40:#42 // append:(I)Ljava/lang/StringBuilder;
#32 = NameAndType #43:#44 // toString:()Ljava/lang/String;
#33 = Class #45 // java/io/PrintStream
#34 = NameAndType #46:#47 // println:(Ljava/lang/String;)V
#35 = Utf8 com/wd/clazz/AnalyseBytecode
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/System
#38 = Utf8 out
#39 = Utf8 Ljava/io/PrintStream;
#40 = Utf8 append
#41 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = Utf8 (I)Ljava/lang/StringBuilder;
#43 = Utf8 toString
#44 = Utf8 ()Ljava/lang/String;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 println
#47 = Utf8 (Ljava/lang/String;)V
{
public com.wd.clazz.AnalyseBytecode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wd/clazz/AnalyseBytecode;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #3 // class java/lang/StringBuilder
13: dup
14: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
17: ldc #5 // String num:
19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: iinc 1, 1
35: iload_1
36: istore_2
37: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
40: new #3 // class java/lang/StringBuilder
43: dup
44: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
47: ldc #5 // String num:
49: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
52: iload_2
53: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
56: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
59: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: return
LocalVariableTable:
Start Length Slot Name Signature
0 63 0 args [Ljava/lang/String;
2 61 1 i I
7 56 2 num I
}
先看第一部分关键代码:
0: iconst_0 // 将 0 压入栈中
1: istore_1 // 将栈顶的值放入变量 1 中并出栈,从 LocalVariableTable 中查询,变量 1 为 i
2: iload_1 // 将变量 1 放入栈顶
3: iinc 1, 1 // 将变量 1 的值加 1
6: istore_2 // 将栈顶的值放入变量 2 中并出栈,从 LocalVariableTable 中查询,变量 2 为 num
敲黑板,关键点解析,3: iinc 这一步虽然将 i 值递增了,但是并没有将递增后的结果重新入栈,所以此时栈顶的值还是旧值 0,6: istore_2 又将栈顶的值赋值给变量 2 ,此时的值还是变量 1 未被重新赋值时的数值,所以此时 num 为 0。
再看第二部分:
32: iinc 1, 1 // 变量 1 递增 1
35: iload_1 // 变量 1 入栈
36: istore_2 // 将栈顶的值赋值给变量 2 并出栈
这里 num 的值就是 i 递增后的值。
总结:i++ 操作,先入栈,再递增,此时栈中的值还是旧值;++i 操作,先递增,再入栈,此时栈中的值是最新值。iload_ 和 iinc 的顺序导致了结果的不同。