如何把i++说的高大上
i++大家都用过,下面这个for循环基本就和helloworld一个级别的,大家写的第一个循环应该就长这样吧。
for(int i = 0 ; i < 10 ; i++){
//do something...
}
那么这个i++从这里看,就是对i进行+1的操作。没毛病吧?
好,上代码:
int i = 1;
System.out.println((i++)/2);
问:执行结果是什么?
无非两种答案,0或者1。(不会吧,不会吧,不会有人说0.5吧?)
实际结果呢。
答案是0。
为什么是0呢,很多人会说:“i++是操作完‘/’再对i进行i+1,所以(i++)/2实际上是1/2,结果是int型的,没有小数,所以是0"
好,那么上下一段代码
int i = 1;
i = (i++)/2;
System.out.println(i);
结果又是什么呢?
无非还是两种答案,0和1。(没有说2和1.5的吧?)
实际结果还是0。
++呢??
从逻辑反推得出0这个结果的过程是这样的。
1. i=1;
2. 然后1/2
3. 对i+1,此时i=2
4. 把第二步的结果0赋值给i
5. 最后i=0
看起来没啥毛病。
实际上是这样的么?程序真的是照着我们想的那样去做的么?
下面看看程序是怎么做的。
首先,java程序是在哪里执行的?都知道是虚拟机(JVM)。
那么虚拟机执行的是什么?是int i = 1么?
肯定不是,int i = 1是程序员写的,给程序员看的,是.java文件。
那么给JVM看的是什么呢?
java运行之前要进行很重要的一步,就是编译(javac),把.java文件编译成.class文件。这个class文件就是给JVM看的。打开这个.class文件,就是JVM看到了。
(这些我们都知道,跟上面的问题有什么关系!!)
(别急别急,来了来了)
cafe babe 0000 003c 001b 0a00 0200 0307
0004 0c00 0500 0601 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 063c 696e
…中间省略
0101 056c b600 0db1 0000 0001 0016 0000
0001 0019 0000 0002 001a
这玩意怎么看的懂。。。(实际上是能看的,class文件有严格的规范,并且每一个指令码都官网可查的)
看不懂没关系,JAVA很贴心的为 (你们这群愚蠢的人类) 程序员考虑。只需要javap -c ***.class,就能显示出大概能看懂的版本了。
> javap -c Test.class
Compiled from "Test.java"
public class com.jcode.taskcenter.Test {
public com.jcode.taskcenter.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: iinc 1, 1
9: iconst_2
10: idiv
11: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
14: return
}
对应的代码就是
public class Test {
public static void main(String[] args) {
int i = 1;
System.out.println((i++)/2);
}
}
这也看不太懂啊!!
没事,咱一行一行看。掐头去尾不看,这里只看main方法里面的内容。(其他的内容如果感兴趣的自行搜索)
main方法里面第一行
0: iconst_1
不认识(不只第一行不认识,后面也就认识个return),没关系,查!
iconst
当int取值-1~5时,JVM采用iconst指令将常量压入栈中。
int取值0~5时JVM采用iconst_0、iconst_1、iconst_2、iconst_3、iconst_4、iconst_5指令将常量压入栈中。
取值-1时采用iconst_m1指令将常量压入栈中。
知识点来了,栈。
这个就得从JVM的结构开始展开了(不知道的自己去查吧,一搜一大把)。
JVM里面有一块区域叫虚拟机栈(VM Stack)
java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。
从上面这个定义可以看出,虚拟机栈里面放着很多java栈,一个java栈中又会包含多个栈帧,栈帧里面又有操作数栈。那到底iconst压入的栈是哪个栈?上面有一句话,每运行一个方法就创建一个栈帧。我们上面的iconst是在main方法里面的,所以压入的就是栈帧中的操作数栈。
好了,第一句明白了,现在我们main方法这个栈帧里面的操作数栈顶有了一个1了。
第二句
1:istore_1
别问,查。
操作码 | 描述 |
---|---|
istore_0 | 将栈顶int类型值出栈保存到局部变量0中。 |
istore_1 | 将栈顶int类型值出栈保存到局部变量1中。 |
istore_2 | 将栈顶int类型值出栈保存到局部变量2中。 |
istore_3 | 将栈顶int类型值出栈保存到局部变量3中。 |
又一个新玩意,局部变量。
刚才说栈帧的时候提到过。
好了,到此为止,我们这个栈帧里面,操作数栈空了,局部变量1里面有个int型的1。(有人问局部变量0去哪了,电脑不都是以0开始计数的么?这里只说局部变量0就是this)
对应代码来看,就是完成了 int i = 1这行代码。
继续看
2: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
这行看起来就是获取了一个静态的资源,不管。
下面重点来了啊
5: iload_1
6: iinc 1, 1
9: iconst_2
10: idiv
直接解释一下
iload_1 将局部变量1的值入栈
iinc 1,1 对局部变量1进行(+1)操作
iconst_2 将int型的2入栈
idiv 将栈顶两int类型数相除,结果入栈。
我们这里打印的结果是除的结果,也就是栈顶的1除以2。结果是int型的0。
至于++,就是iinc 1,1这行了。现在来看,++是在/之前执行的。而不是之前说的先执行除法,再执行++。但是操作的位置不一样,±*/这类的操作的是操作数栈里面的值,而++操作的是局部变量里面的值。
现在再看上面第二个代码
int i = 1;
i = (i++)/2;
System.out.println(i);
不用看指令码就很明了了吧?
- 给i赋值就是把栈顶的值存到局部变量。
- 栈顶的值是除法得来的
那么除法做完的时候,操作数栈顶是0,[局部变量1]是 2。然后把栈顶的0弹出保存到局部变量1。最后i=0。
现在把指令码发出来印证一下
0: iconst_1
1: istore_1
2: iload_1
3: iinc 1, 1
6: iconst_2
7: idiv
8: istore_1
9: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_1
13: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
16: return
前面是一样的,在idiv后面多了一行istore_1,就是之前说过的把栈顶的值放到局部变量1。
一个看起来简单的i++,可以展开写这么一大堆。有什么用?i++还是i++。
那么设想一个场景,刚毕业找工作,面试官问个初级的问题,i++和i=i+1有什么区别。是简单说一句“结果上没有区别”,还是直接深入,引入指令码,引入栈帧,然后引出JVM相关的。对于一个毕业生来说,这些就已经足够拿到一个offer了。
同样的,还有类似String判断==的问题,表面上看起来是String的问题,实际上问的是JAVA内存模型。看问题不要流于表面,就算问题回答对了,讲不出原理来,依然还是不懂。