Java中的i++和++i,你真的懂了吗?
思考一下下面这段代码输出什么?
public static void main(String[] args) {
int j=0;
for(int i=0;i<10;i++){
j=(j++);
}
System.out.println(j);
}
相信你已经有了答案,上面这段代码输出的是10吗?正确答案是0。那么就要思考一下,代码的执行顺序,难道不是j++后再赋值给j吗?答案当然是啊。那为什么结果会是0呢?要解答这个疑惑,就要了解一下栈帧中的局部变量表和操作数栈。关于JVM的内存结构,可以到我的上一篇博客了解。
字节码
一个简单的Helloworld.java文件
public class Helloworld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
经过编译成Helloworld.class后是这样的:
从这些字节码中理解java中的操作指令,这不是开玩笑吗?这哪是给人看的啊!!对,这还真不是给人看的,这是给机器看的。那人要用什么看呢?Oracle提供javap工具来反编译*.class文件,IDEA中可以通过下图操作使用javap工具。
键入javap -v -p *.class文件所在路径,回车
Classfile /D:/IDEA/WorkSpace/out/production/JavaBasic/self_increment/Test1.class
Last modified 2020年5月30日; size 614 bytes
SHA-256 checksum f2226c103b1fcddfbc1c883ca6ae2a58564043c8522365647b306e4376596cad
Compiled from "Test1.java"
public class self_increment.Test1
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // self_increment/Test1
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #26.#27 // java/io/PrintStream.println:(I)V
#4 = Class #28 // self_increment/Test1
#5 = Class #29 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lself_increment/Test1;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 i
#16 = Utf8 I
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 j
#20 = Utf8 StackMapTable
#21 = Utf8 SourceFile
#22 = Utf8 Test1.java
#23 = NameAndType #6:#7 // "<init>":()V
#24 = Class #30 // java/lang/System
#25 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#26 = Class #33 // java/io/PrintStream
#27 = NameAndType #34:#35 // println:(I)V
#28 = Utf8 self_increment/Test1
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (I)V
{
public self_increment.Test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lself_increment/Test1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: bipush 10
7: if_icmpge 21
10: iload_1
11: iinc 1, 1
14: istore_1
15: iinc 2, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 10
line 6: 15
line 9: 21
line 10: 28
LocalVariableTable:
Start Length Slot Name Signature
4 17 2 i I
0 29 0 args [Ljava/lang/String;
2 27 1 j I
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 4
locals = [ int, int ]
frame_type = 250 /* chop */
offset_delta = 16
}
SourceFile: "Test1.java"
反编译后,就是给人看的了。我们可以看到,类文件里面有Constant pool(常量池),构造方法public self_increment.Test1()和main方法public static void main(java.lang.String[])中的指令集,LocalVariableTable(局部变量表)等等。
JVM虚拟机指令
JVM虚拟机指令繁多,这里只把用到的解释一下。
- iconst_0:将值为0的int类型推送至栈顶,(0~5通过该指令,-1通过指令iconst_m1)。
- istore_1:将栈顶的值存入slot为1,即1号槽位中。
- iload_2:将局部变量表2号槽位中的值压入栈顶。
- bipush :将单字节常量(-128~127)压入栈顶。
- if_icmpge :比较栈顶两个int类型的数值,当前者大于等于后者时跳转。
- iinc :完成int类型数值自增自减。
- getstatic:获取指定类的静态字段,并将值压入栈顶。
- invokevirtual:虚方法调用。
了解了这些内容,就可以真正开始解答疑惑了。
分析
我们主要关注main方法中代码的执行,随着main方法的调用,就会在java虚拟机栈创建一个栈帧并入栈。main方法栈帧中包括操作数栈和局部变量表,当然还有其他,这里不多阐述。
LocalVariableTable:
Start Length Slot Name Signature
4 17 2 i I
0 29 0 args [Ljava/lang/String;
2 27 1 j I
我们看到,局部变量表中有三个Slot(槽位),槽位中存储的值就是对应的变量名的值。
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: bipush 10
7: if_icmpge 21
10: iload_1
11: iinc 1, 1
14: istore_1
15: iinc 2, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
mian方法中的一系列虚拟机指令,都是在操作数栈完成的。
我们逐条分析指令
- iconst_0,将值为0的int类型压入栈顶。
- istore_1,将栈顶的值出栈并存入1号槽位,即j=0,对应源码中将局部变量j初始化为0。
- iconst_0,同上。
- istore_2,同理,i=0。
- iload_2,将二号槽位上i的值入栈。
- bipush 10,将10压入栈顶。
- if_icmpge 21,比较栈顶两个int类型的值,当前者(先入栈的i)大于等于后者(后入栈的10),跳转到21行。显然这是for循环条件不满足后跳出,条件满足时则继续往下走。
- iload_1,将1号槽位上,即j的值入栈。
- iinc 1, 1,将1号槽位,即j的值+1,现在j=1。
- istore_1,将栈顶的值,即0存入1号槽位,可以看到,这一步将j的值重新赋值为0。
- iinc 2, 1,将2号槽位,即i的值+1。
- goto 4,跳转到第四行。
- getstatic #2,根据#2在常量池中找,javap工具给出的注释内容即#2在常量池对应的是System.out对象,它的类型是PrintStream。
- iload_1,将1号槽位j的值入栈。
- invokevirtual #3 ,根据#3在常量池中找,它对应的是println:(I)V,参数是I,即Integer。
- return,main方法结束,对于的栈帧出栈。
我们可以看到,在每一次的for循环中,局部变量表中j的值+1后,istore_1这条指令将栈顶的0重新存入j中,每一次for循环后,j的值都是0。
③istore_1,就将栈顶的0出栈并存入局部变量表的1号槽位,即j中。
思考
将文章开头那段代码中j=(j++)
换为j=(++j)
,结果又是什么?
总结:
- iinc这个指令是在局部变量表中完成,无需压入操作数栈。
- 对于++i这个操作,JVM先执行iinc指令再执行iload指令。
- 对于i++这个操作,JVM先执行iload指令再执行iinc指令。
- 对于–i和i–,与++i和i++同理,只是iinc指令后的第二个数为-1,表示自减而已。
来源:oschina
链接:https://my.oschina.net/u/4278828/blog/4298037