Java第一課

你的解釋不是我想要的

“同學(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é)辭退了。”

起床,刷牙洗臉,上班。

今天又會有什么好玩的需求?

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容