这几天抽着一些时间,把Java的class文件结构研究了一下,再后来就想起了这个令人厌烦的问题,想从字节码指令的角度看看,java到底是怎么处理这个的
先看一段java代码
打印的i和j分别是多少呢?
先分析i++:
这三句对应的字节码为:
java虚拟机内存空间中存在一个叫java方法栈的区域,在这里为每个方法提供一个栈帧,在栈帧中存放了该方法的局部变量表,操作栈,动态链接,方法出口等信息。上面字节码中指令所指的栈应该就是这个操作栈吧(个人理解,不保证正确~)。
分析完i++,下面来看看++j的原理
对应的字节码为:
总结
i++和++i的区别就是在进入操作栈和自加的顺序,呃,说完怎么跟没说一样……
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6.1
- public class Test {
- public static void main(String[] args) {
- int i=0;
- i=i++;
- System.out.println(i);
- }
- }
- 结果是0
- 为什么是0 因为:
- 是因为Java编译器的原因:我们来看看编译后的字节码
- 0 iconst_0 //将int型0推送至栈顶
1 istore_1 //将栈顶int型数值存入第二个本地变量 i=0 (int i=0)
2 iload_1 //将第二个int型本地变量推送至栈顶 然后将i推送至栈顶 0
3 iinc 1 1 //将指定int型变量增加指定值(i++, i--, i+=2)完成i++
4 istore_1 //将栈顶int型数值存入第二个本地变量 将栈元素赋值给了i i=0
5 getstatic java/lang/System/out Ljava/io/PrintStream;
6 iload_1
- public static void main(String[] args) {
- int i=0;
- i++;
- System.out.println(i);
- }
- 字节码为:
- iconst_0 //将int型0推送至栈顶
- istore_1 //将栈顶int型数值存入第二个本地变量 i=0 (int i=0)
- iinc 1 1 //将指定int型变量增加指定值(i++, i--, i+=2)完成i++
- public static void main(String[] args) {
- int i=0;
- int k=i++;
- System.out.println(i);
- }
- 字节码为:
- 0 iconst_0 //将int型0推送至栈顶
1 istore_1 //将栈顶int型数值存入第二个本地变量 i=0 (int i=0)
2 iload_1 //将第二个int型本地变量推送至栈顶 然后将i推送至栈顶 0
3 iinc 1 1 //将指定int型变量增加指定值(i++, i--, i+=2)完成i++
4 istore_2 //将栈顶int型数值存入第三个本地变量 将栈元素赋值给了i i=0
5 getstatic java/lang/System/out Ljava/io/PrintStream;
6 iload_1
- public static void main(String[] args) {
- int i=0;
- int k=++i;
- System.out.println(i);
- }
- 字节码为:
- 0 iconst_0
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_2
- 对比而言,对于i++而言,i=i++指令多了两步,2和4
其实这两步是赋值符号引起的,有意思的是第二步出现的时机,是在iinc之前,这就是因为java lang spec中规定的。 - java编译器对于++i 并不会生成2和4
- int i=0 i=i++ 或者 int i=0 int j= i++ 相当于 int temp = i i++ i=temp int temp = i i++ j=temp
- int i=0 i=++i 相当于 ++i temp = i i=temp;
- 总结:java编译器对于i++会先将i的值保存至另一变量 然后在对i++,另一变量仍没有改变。 而对于++i 是先对i++ 然后保存到另一变量 然后赋值。
先看4个题目:
①int i = 0;
i = i++;
②int i = 0;
i = ++i;
③int i = 0;
int j = 0;
j = i++ + i++;
④ int i = 0;
int j = 0;
j = i++ + i++ + i++;
每道题里的i和j都是多少?
结果分别是
①i = 0,
//出现0可以这样简单理解:自增是单独的一个操作,自增的结果不会体现在变量i上。i++,本身就是逻辑运算(i=i+1)的缩写。
//容易混淆的地方就在于定义的变量i和自增操作的变量i名称相同,如果写成这样i=i;temp=temp+1;的两行代码,就没有疑惑了
②i = 1,
③i = 2,j = 1,
④i = 3,j = 3。
i++和++i的问题,困扰很多人。现在通过分析字节码,来确定这两条语句究竟是怎样执行的。
先给出今天要用到的字节码的含义
Bytecode | Stack before->after | Description |
iconst_0 | ->0 | Loads the int value 0 onto the stack |
istore_1 | value-> | Store int value into variable 1 |
istore_2 | value-> | Store int value into variable 2 |
iinc | No change | Increment local variable #index by signed byte const |
iload_1 | ->value | Loads an int value from variable 1 |
iadd | value 1,value 2->result | Adds 2 ints together |
说明两点需要注意的地方:
①iinc操作是有参数的,但是在此忽略,简写为iinc,此操作对应于自加操作,并且该操作不对stack有任何改变;
②iadd操作过后只在stack中保留结果result。
接下来是四段程序主要的字节码:
①iconst_0 ②iconst_0 ③iconst_0 ④iconst_0
istore_1 istore_1 istore_1 istore_1
iload_1 iinc 1,1 iconst_0 iconst_0
iinc 1,1 iload_1 istore_2 istore_2
istore_1 istore_1 iload_1 iload_1
iinc 1,1 iinc 1,1
iload_1 iload_1
iinc 1,1 iinc 1,1
iadd iadd
istore_2 iload_1
iinc 1,1
iadd
istore_2
现在解释①。第一步在stack中存入一个int常量0;第二步把它赋值给第一个变量,即我们的i;第三步把第一个变量i的值存入到stack中;第四步在i自身的空间进行自加,而第三步存入到stack中的值没有变;第五步把第三步存入到stack中的值再赋值给第一个变量i。也就是说,i真的是进行自加了,但是被自己原来的值覆盖掉了。从这里我们可以看出,自加操作比赋值操作的优先级高。
再看②。和①比较起来,区别就在于,++i是先进行自加,然后把自加后的值存入到stack中,所以最后赋值给i的是1,不是0。
③呢?首先,③比前两个多了一个变量,但这不是重点。往下看,把第一个变量i的值(0)存入到stack中,i自加,再第一个变量i的值(1)存入到stack中,再自加。此时stack中有两个值了,0和1,进行加操作后的结果是1,然后赋值给第二个变量j。这里颠覆了我们以前被告知的,加的运算优先级比自加高。这就是困扰我们的,其实,只要把值读入到stack中,接下来就进行自加,只有得到了两个参数,才进行加操作。
来看④。前面都和③一样,只到进行了第一次加操作,得到的结果(1)保存在stack中,没有赋值给j,然后再次读入i的值(2)到stack,i自加,得到加操作的两个参数,1和2,进行第二次加操作的结果是3,赋值给j。
到此,问题都解决了。