Java第一課

weixin_34107955發表於2018-06-11

你的解釋不是我想要的

“同學們好,我是教授你們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中,這個翻譯的動作,是由JVM,Java虛擬機器來完成。

大家都知道Java是跨平臺的,所謂“Write Once, Run Anywhere”, 同樣一份程式碼,可以在不同的平臺上執行,不像別的語言,比如C,也許這段程式碼在Linux上正常,去到OS X就有Bug了。

那麼Java是如何實現跨平臺的呢?簡單說,靠的就是JVM這個翻譯官。

你寫好的程式碼,會被編譯成一個.class檔案,也就是Java位元組碼檔案,這裡面記錄的是一系列要在JVM執行的指令。

接著,你拿著這份位元組碼指令,去到任意一個JVM,Linux的JVM也好,OS X的也好,它們都會幫你把它翻譯成對於平臺的機器指令。這就實現了跨平臺、

Java位元組碼是國際通用語言(英語),JVM是翻譯官。

反彙編

回到我們的問題,++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值,壓到運算元棧中。

7143349-b32d2e91ccacf8d5.png

2、istore_1
從運算元棧中彈出一個值,存放到區域性變數表index為1的位置(為什麼不是0,思考題)

pop之前:

7143349-a28e88cb12ce6388.png

pop之後:

7143349-3c00832edc9dca02.png

以上兩條指令對應的是第一行程式碼 int i = 0:

它實現了給i賦值,並且把i放到區域性變數表的功能。

下面再來看看 i = i++ 對應的指令。

3、iload_1
把區域性變數表中,index=1位置的值,壓到運算元棧中。

7143349-c0a0ca6da2be9569.png

4、iinc 1, 1
對區域性變數表index=1位置的值,進行加1操作。

7143349-87b6d815490f69a3.png

iinc指令包含兩個引數:

  • 第一個是index,代表要操作是區域性變數表哪個位置的值;
  • 第二個是const,代表要加多少;

現在區域性變數表裡的i其實是等於1的,可是為什麼最後列印出來還是0呢?

問題出在最後一條指令。

5、istore_1
從運算元棧中彈出一個值,將它賦值給區域性變數表中,index為1位置上的值。

pop之前:


7143349-072376d14cc8f0ed.png

pop之後:


7143349-2a99d378b7ba8070.png

完蛋,這下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執行時資料區域中,很小的一塊,完整的模型圖是這樣:

7143349-c8235ef5d762e2dc.jpg

運算元棧和區域性變數表,位於圖中的JVM Stack中,也就是我們常說的虛擬機器棧。

End

這堂課的重點,並不在於跟大家解釋i++和++i的區別,而是要給大家引入一個Java中十分重要的觀察角度——JVM.

你寫的程式碼,只是表象,程式不一定按照表象去執行。

萬一發現很奇怪的現象了,莫慌,別忘了中間還有個JVM在作祟。

......

忽然,鬧鐘響了。

“傻蛋,怎麼老是做這個夢。你早就因為開不了課被大學辭退了。”

起床,刷牙洗臉,上班。

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

參考

相關文章