美團一面問我i++跟++i的區別是什麼

明智说發表於2024-05-18

美團一面問我i++跟++i的區別是什麼

面試官:“i++跟++i的區別是什麼?”

我:“i++是先使用然後再執行+1的操作,++i是先執行+1的操作然後再去使用i”

面試官:“那你看看下面這段程式碼,執行結果是什麼?”

public static void main(String[] args) {
    int j = 0;
    for (int i = 0; i < 10; i++) {
        j = (j++);
    }
    System.out.println(j);
}

我:“我猜他肯定不是10”

面試官:

我:“哈哈.....,開個玩笑,結果為0啦”

面試官:“為什麼呢?”

我:“簡單來說的話,j++這個表示式每次返回的都是0,所以最終結果就是0”

對應前文提到過的:i++這種寫法是先使用,再執行+1操作,如果不理解請暫停多思考思考

面試官:“小夥子不錯,那你能從更底層的角度講一講為什麼嘛?”

首先我們知道,JVM的執行時資料區域是分為好幾塊的,具體分佈如下圖所示:

現在我們主要關注其中的虛擬機器棧,關於虛擬機器棧,我們需要了解的是:

  1. Java虛擬機器棧是由一個個棧幀組成,執行緒在執行一個方法時,便會向棧中放入一個棧幀。
  2. 每一個方法所對應的棧幀又包含了以下幾個部分
    • 區域性變數表
    • 運算元棧
    • .........

其中的區域性變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用。

區域性變數表的最小儲存單元為Slot(槽),其中64位長度的long和double型別的資料會佔用2個Slot,其餘的資料型別只佔用1個。因此可以直接透過下標來進行資料訪問

運算元棧對於資料的儲存跟區域性變數表是一樣的,但是跟區域性變數表不同的是,運算元棧對於資料的訪問不是透過下標而是透過標準的棧操作來進行的(壓入與彈出)

資料的計算是由CPU完成的,彈棧的目的就是將資料壓入到CPU中

接下來我們分析下面這段程式碼在位元組碼層面的執行過程:

// 為方便閱讀將對應程式碼也放到這裡
public static void main(String[] args) {
 int j = 0;
 for (int i = 0; i < 10; i++) {
     j = (j++);
 }
 System.out.println(j);
}

我們進入到這段程式碼編譯好的.class檔案目錄下執行:javap -c xxx.class,得到其位元組碼如下:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0    // 將常數0壓入到運算元棧頂
       1: istore_1    // 將運算元棧頂元素彈出並壓入到區域性變數表中1號槽位,也就是j=0
       2: iconst_0    // 將常數0壓入到運算元棧頂
       3: istore_2	  // 將運算元棧頂元素彈出並壓入到區域性變數表中2號槽位,也就是i=0
       4: iload_2     // 將2號槽位的元素壓入運算元棧頂
       5: bipush        10   // 將常數10壓入到運算元棧頂,此時運算元棧中有兩個數(常數10,以及i)
       7: if_icmpge     21	 // 比較運算元棧中的兩個數,如果i>=10,跳轉到第21行
      10: iload_1			 // 將區域性變數表中的1號槽位的元素壓入到運算元棧頂,就是將j=0壓入運算元棧頂
      11: iinc          1, 1 // 將區域性變數表中的1號元素自增1,此時區域性變數表中的j=1

      14: istore_1			 // 將運算元棧頂的元素(此時棧頂元素為0)彈出並賦值給區域性變數表中的1號							      槽位(一號槽位本來已經完成自增了,但是又被賦值成了0)
      
      15: iinc          2, 1 // 將區域性變數表中的2號槽位的元素自增1,此時區域性變數表中的2號元素值為1,也就是i=1
      
      18: goto          4	 // 第一次迴圈結束,跳轉到第四行繼續迴圈
      21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      24: iload_1
      25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      28: return

我們著重關注第10,11,14行位元組碼指令,用圖表示如下:

可以看到本來區域性變數表中的j已經完成了自增(iinc指令是直接對區域性變數進行自增),但是在進行賦值時是將運算元棧中的資料彈出,但是運算元棧的資料並沒有經過計算,所以每次自增的結果都被覆蓋了,最終結果就是0。

我們平常說的i++是先使用,然後再自增,而++i是先自增再使用。這個到底怎麼理解呢?如果站在JVM的層次來講的話,應該這樣說:

  1. i++是先被運算元棧拿去用了(先執行的load指令),然後再在區域性變數表中完成了自增,但是運算元棧中還是自增前的值
  2. 而++1是先在區域性變數表中完成了自增(先執行innc指令),然後再被load進了運算元棧,所以運算元棧中儲存的是自增後的值

這就是它們的根本區別。

關於i++的執行過程,我這裡也給出一個程式及編譯後的結果

public static void main(String[] args) {
    int i = 0;
    i = ++i;
    System.out.println(i);
}
>  0 iconst_0
>  1 istore_1
>  2 iinc 1 by 1
>  5 iload_1
>  6 istore_1
>  7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
> 10 iload_1
> 11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
> 14 return

大家可以自行分析


作者簡介

大三退學,創業、求職、自考,一路升級

7年it從業經驗,多個開源社群contributor

半自由職業,新時代數字遊民

自媒體創業,專注分享成長路上的所悟所得

長期探索 個人成長職業發展副業探索

相關文章