你的解釋不是我想要的
“同學(xué)們好,我是教授你們Java101課程的S老師。下面開始我們的第一堂課吧?!?/p>
“Java安裝、編輯器安裝、以及運行起hello world代碼,我已經(jīng)在課前預(yù)習(xí)郵件里,告訴大家要怎么做了,不知道大家完成的怎么樣?”
“老師,您的郵件里就一句話,‘請自行Google’ ...”
“沒錯。”
其實我內(nèi)心OS是:如果臺下大部分學(xué)生,都完成不了預(yù)習(xí)任務(wù),嗯,那這門課又開不成了,我又可以安心做研究。
不過為了讓這個故事繼續(xù)下去,我們姑且假設(shè)大部分學(xué)生都完成了預(yù)習(xí)任務(wù)吧。
“嗯,同學(xué)們很出色,下面再來一起看看這兩段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);
}
}
“相信大家也都知道運行結(jié)果了,第一段代碼是0,第二段代碼是1。好,我們的第一堂課就是這樣,大家還有什么疑問嗎?”
大概過了半分鐘,臺下有個同學(xué)問道,“老師,我想知道為什么?為什么只是換了下順序,結(jié)果就不一樣了?”
這是我期待已久的問題,對,就是簡簡單單三個字,“為什么”
旁邊一同學(xué),說道,“這個我知道。i =i++,會先賦值,再加一,所以結(jié)果是0,而i = ++i,會先把i加一,然后再賦值,所以結(jié)果是1”
全場感嘆,都向那位同學(xué)投以敬佩的目光,畢竟他的理論足以解釋現(xiàn)象。
唯有剛剛提問的同學(xué),說了一句,“你的解釋不是我想要的......”
翻譯官
這堂Java第一課的高潮終于到來了,我很激動。
剛剛這位同學(xué)的解釋,不可謂不對,但是終究沒說到點上。
i =i++,會先賦值,再加一,所以結(jié)果是0,這個解釋很正確,但是理由在哪?
這只是你的片面之詞呢?還是道聽途說所得?這個解釋不足以服眾。
你寫的代碼,是高級語言,是給人看的,機器可看不懂。
所以在你寫的代碼,到機器開始執(zhí)行中間,肯定有一個翻譯的過程。
Java中,這個翻譯的動作,是由JVM,Java虛擬機來完成。
大家都知道Java是跨平臺的,所謂“Write Once, Run Anywhere”, 同樣一份代碼,可以在不同的平臺上運行,不像別的語言,比如C,也許這段代碼在Linux上正常,去到OS X就有Bug了。
那么Java是如何實現(xiàn)跨平臺的呢?簡單說,靠的就是JVM這個翻譯官。
你寫好的代碼,會被編譯成一個.class文件,也就是Java字節(jié)碼文件,這里面記錄的是一系列要在JVM執(zhí)行的指令。
接著,你拿著這份字節(jié)碼指令,去到任意一個JVM,Linux的JVM也好,OS X的也好,它們都會幫你把它翻譯成對于平臺的機器指令。這就實現(xiàn)了跨平臺、
Java字節(jié)碼是國際通用語言(英語),JVM是翻譯官。
反匯編
回到我們的問題,++i和i++為什么會不一樣呢?
這就要看這兩行高級語言代碼,轉(zhuǎn)成字節(jié)碼指令之后是什么樣子了。
先來看看HelloWorld-1。首先使用javac把你寫的高級語言,也就是java文件,編譯成字節(jié)碼文件。我已經(jīng)把源代碼中的System.out.println(i)刪掉,這樣我們就可以專心觀察i++和++i:
javac HelloWorld.java
可以看到HelloWorld.java同級目錄下,出現(xiàn)了一個HelloWorld.class文件。
class文件里面都是二進制的數(shù)據(jù)。為什么是二進制?因為這些都是告訴JVM要做什么事情的指令,而機器只看得懂0101之類的二進制。
所以,我們需要對這個二進制數(shù)據(jù),進行反匯編,把它變成人類看得懂的語言,來看看這些二進制數(shù)據(jù)都在說些什么,這里我們用到j(luò)avap:
javap -c HelloWorld.class
命令執(zhí)行后,控制臺打印出一系列的字節(jié)碼指令,其中main函數(shù)的字節(jié)碼指令如下:
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
這一串的指令,主要涉及到兩個數(shù)據(jù)結(jié)構(gòu),一個是操作數(shù)棧(operand stack),另一個是局部變量表(local variable)。前者是棧,后者是數(shù)組。
那么這些指令都是什么意思?
不急,下面圖文并茂,給你解釋。
棧和數(shù)組的故事
1、iconst_0
把一個值為0的int值,壓到操作數(shù)棧中。

2、istore_1
從操作數(shù)棧中彈出一個值,存放到局部變量表index為1的位置(為什么不是0,思考題)
pop之前:

pop之后:

以上兩條指令對應(yīng)的是第一行代碼 int i = 0:
它實現(xiàn)了給i賦值,并且把i放到局部變量表的功能。
下面再來看看 i = i++ 對應(yīng)的指令。
3、iload_1
把局部變量表中,index=1位置的值,壓到操作數(shù)棧中。

4、iinc 1, 1
對局部變量表index=1位置的值,進行加1操作。

iinc指令包含兩個參數(shù):
- 第一個是index,代表要操作是局部變量表哪個位置的值;
- 第二個是const,代表要加多少;
現(xiàn)在局部變量表里的i其實是等于1的,可是為什么最后打印出來還是0呢?
問題出在最后一條指令。
5、istore_1
從操作數(shù)棧中彈出一個值,將它賦值給局部變量表中,index為1位置上的值。
pop之前:

pop之后:

完蛋,這下i又變成0了。
至于 i = ++i為什么最后是1 ,請大家按照上面的思路,自行分析。
其實兩者的差別只在iload_1和iinc 1, 1的順序上。
i = ++i,iinc 1, 1在前,iload_1在后,所以最后結(jié)果是1.
上面這些指令的含義,不需要刻意去記,有JVM規(guī)范可以查看:The Java Virtual Machine Instruction Set
這堂課提到的操作數(shù)棧和局部變量表,只是JVM運行時數(shù)據(jù)區(qū)域中,很小的一塊,完整的模型圖是這樣:

操作數(shù)棧和局部變量表,位于圖中的JVM Stack中,也就是我們常說的虛擬機棧。
End
這堂課的重點,并不在于跟大家解釋i++和++i的區(qū)別,而是要給大家引入一個Java中十分重要的觀察角度——JVM.
你寫的代碼,只是表象,程序不一定按照表象去執(zhí)行。
萬一發(fā)現(xiàn)很奇怪的現(xiàn)象了,莫慌,別忘了中間還有個JVM在作祟。
......
忽然,鬧鐘響了。
“傻蛋,怎么老是做這個夢。你早就因為開不了課被大學(xué)辭退了。”
起床,刷牙洗臉,上班。
今天又會有什么好玩的需求?
參考
- The Java Virtual Machine Instruction Set
- 《文學(xué)回憶錄》