“同学们好,我是教授你们Java101课程的S老师。下面开始我们的第一堂课吧。”
“Java安装、编辑器安装、以及运行起hello world代码,我已经在课前预习邮件了告诉大家怎么做了,不知道大家完成的怎么样?”
“老师,您的邮件里就一句话,‘请自行Google’ …”
“没错。”
其实我内心OS是:如果台下大部分学生,都完成不了预习任务,嗯,那这门课又开不成了,我又可以安心做研究。
不过为了让这个故事继续下去,我们姑且假设大部分学生都完成了预习任务吧。
“嗯,同学们很出色,下面再来一起看看这两段Hello World代码”
HelloWorld-1:
public class HelloWorld { public static void main(String[] args) { int i = 0; i = i++; System.out.println(i); } }
HelloWorld-2:
public class HelloWorld { public static void main(String[] args) { int i = 0; i = ++i; System.out.println(i); } }
“相信大家也都知道运行结果了, 第一段代码是0,第二段代码是1 。好,我们的第一堂课就是这样,大家还有什么疑问吗?”
大概过了半分钟,台下有个同学问道,“老师,我想知道为什么?为什么只是换了下顺序,结果就不一样了?”
这是我期待已久的问题,对,就是简简单单三个字,“为什么”
旁边一同学,说道,“这个我知道。i =i++,会先赋值,再加一,所以结果是0,而i = ++i,会先把i加一,然后再赋值,所以结果是1”
全场感叹,都向那位同学投以敬佩的目光,毕竟他的理论足以解释现象。
唯有刚刚提问的同学,说了一句,“ 你的解释不是我想要的 ……”
这堂Java第一课的高潮终于到来了,我很激动。
刚刚这位同学的解释,不可谓不对,但是终究没说到点上。
i =i++,会先赋值,再加一,所以结果是0, 这个解释很正确,但是理由在哪?
你写的代码,是高级语言,是给人看的,机器可看不懂。
所以在你写的代码,到机器开始执行中间,肯定有一个翻译的过程。
大家都知道Java是跨平台的,所谓“Write Once, Run Anywhere”, 同样一份代码,可以在不同的平台上运行,不像别的语言,比如C,也许这段代码在Linux上正常,去到OS X就有Bug了。
那么Java是如何实现跨平台的呢?简单说,靠的就是JVM这个翻译官。
你写好的代码,会被编译成一个.class文件,也就是Java字节码文件,这里面记录的是一系列要在JVM执行的指令。
接着,你拿着这份字节码指令,去到任意一个JVM,Linux的JVM也好,OS X的也好,它们都会帮你把它翻译成对于平台的机器指令。这就实现了跨平台、
回到我们的问题,++i和i++为什么会不一样呢?
这就要看这两行高级语言代码,转成字节码指令之后是什么样子了。
先来看看HelloWorld-1。首先使用javac把你写的高级语言,也就是java文件,编译成字节码文件。我已经把源代码中的System.out.println(i)删掉,这样我们就可以专心观察i++和++i:
javac HelloWorld.java
可以看到HelloWorld.java同级目录下,出现了一个HelloWorld.class文件。
class文件里面都是二进制的数据。为什么是二进制?因为这些都是告诉JVM要做什么事情的指令,而机器只看得懂0101之类的二进制。
所以,我们需要对这个二进制数据,进行反汇编,把它变成人类看得懂的语言,来看看这些二进制数据都在说些什么,这里我们用到javap:
javap -c HelloWorld.class
命令执行后,控制台打印出一系列的字节码指令,其中main函数的字节码指令如下:
public static void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_1 7: return
这一串的指令,主要涉及到两个数据结构, 一个是操作数栈(operand stack),另一个是局部变量表(local variable)。前者是栈,后者是数组。
那么这些指令都是什么意思?
不急,下面图文并茂,给你解释。
1、iconst_0
把一个值为0的int值,压到操作数栈中。
2、istore_1
从操作数栈中弹出一个值,存放到局部变量表index为1的位置(为什么不是0,思考题)
pop之前:
pop之后:
以上两条指令对应的是第一行代码 int i = 0:
它实现了给i赋值,并且把i放到局部变量表的功能。
下面再来看看 i = i++ 对应的指令。
3、iload_1
把局部变量表中,index=1位置的值,压到操作数栈中。
4、iinc 1, 1
对局部变量表index=1位置的值,进行加1操作。
iinc指令包含两个参数:
现在局部变量表里的i其实是等于1的,可是为什么最后打印出来还是0呢?
问题出在最后一条指令。
5、istore_1
从操作数栈中弹出一个值,将它赋值给局部变量表中,index为1位置上的值。
pop之前:
pop之后:
完蛋,这下i又变成0了。
至于 i = ++i为什么最后是1 ,请大家按照上面的思路,自行分析。
其实两者的差别只在iload_1和iinc 1, 1的顺序上。
i = ++i,iinc 1, 1在前,iload_1在后,所以最后结果是1.
上面这些指令的含义,不需要刻意去记,有JVM规范可以查看: The Java Virtual Machine Instruction Set
这堂课提到的操作数栈和局部变量表,只是JVM运行时数据区域中,很小的一块,完整的模型图是这样:
操作数栈和局部变量表,位于图中的JVM Stack中,也就是我们常说的虚拟机栈。
这堂课的重点,并不在于跟大家解释i++和++i的区别,而是要给大家引入一个Java中十分重要的观察角度——JVM.
你写的代码,只是表象,程序不一定按照表象去执行。
万一发现很奇怪的现象了,莫慌,别忘了中间还有个JVM在作祟。
……
忽然,闹钟响了。
“傻蛋,怎么老是做这个梦。你早就因为开不了课被大学辞退了。”
起床,刷牙洗脸,上班。
今天又会有什么好玩的需求?
Previous
ThreadLocal趣谈 —— 杨过和他的四个冤家