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

 腳本之家 設爲“星標
第一時間收到文章更新

來源 | 明智說(ID:programerDmz)
美團一面問我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          11 // 將局部變量表中的1號元素自增1,此時局部變量表中的j=1

      14: istore_1    // 將操作數棧頂的元素(此時棧頂元素爲0)彈出並賦值給局部變量表中的1號             槽位(一號槽位本來已經完成自增了,但是又被賦值成了0)
      
      15: iinc          21 // 將局部變量表中的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
      28return

我們着重關注第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 
10 iload_1
11 invokevirtual #3 
14 return

大家可以自行分析

  推薦閱讀:
  1. 顯微鏡下的 i++ 與 ++i
  2. 面試官又整新活,居然問我for循環用i++和++i哪個效率高?
  3. 爲什麼說++i的效率比i++高?
  4. 爲什麼服務器內存硬件上的黑色顆粒這麼多?

  5. Oracle最終還是殺死了MySQL!