設(shè)置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

顯微鏡下的 i++ 與 ++i

低并發(fā)編程 2022/9/29 11:18:18 責(zé)編:子非

本文來自微信公眾號(hào):低并發(fā)編程 (ID:dibingfa),作者:閃客

注意,以下討論的語言是 Java

這個(gè)問題被網(wǎng)上的好多文章寫爛了,但基本重復(fù)度很高,我看過后的感覺是,大部分都是錯(cuò)誤的、誤導(dǎo)讀者的。

隨便百度一下,我們打開第一條。

上來先說個(gè)結(jié)論

i++ 先賦值再運(yùn)算,例如 a=i++,先賦值 a=i,后運(yùn)算 i=i+1,所以結(jié)果是 a==1

++i 先運(yùn)算再賦值,例如 a=++i,先運(yùn)算 i=i+1,后賦值 a=i,所以結(jié)果是 a==2

然后給了成噸的例子來說明

public class Test3 {
public static void main(String[] args) {
int y=0; 
//注意 "=" 是賦值,"==" 才是相等
//這里的 y=++y 是先運(yùn)算在賦值
y=++y;// y==0,++y==y+1; 結(jié)果 y=++y == y+1 == 0+1 ==1
y=++y;// y==1,++y==y+1; 結(jié)果 y=++y == y+1 == 1+1 ==2
y=++y;// y==2,++y==y+1; 結(jié)果 y=++y == y+1 == 2+1 ==3
y=++y;// y==3,++y==y+1; 結(jié)果 y=++y == y+1 == 3+1 ==4
y=++y;// y==4,++y==y+1; 結(jié)果 y=++y == y+1 == 4+1 ==5
System.out.println("y="+y);//5
int i =0;
// i==0,i++==0; 結(jié)果 i=i++ == (記住先賦值后運(yùn)算)
i=i++;
i=i++;
i=i++;
i=i++;
i=i++;
System.out.println("i="+i);//0
System.out.println("================");//1
}
}

首先這個(gè)例子沒有任何代表性;

其次得出的結(jié)論也是極其誤導(dǎo)人的;

但最關(guān)鍵的是,這無法幫助你真正理解 i++ 和 ++i 的本質(zhì)是什么

所以 i++ 和 ++i 的區(qū)別請(qǐng)聽我說

先忘掉什么“先賦值、后運(yùn)算”

別著急,慢慢來,忍住看到最后

i++ 和 ++i 字節(jié)碼

查看字節(jié)碼用 javap 命令,或者直接用 idea 的插件,這里不做過多介紹

在某方法里寫上這樣一段代碼

public void ipp() {
    int i = 1;
    i++;
}

查看其字節(jié)碼

iconst_1
istore_1
iinc 1 1
return

然后我們?cè)趯懮线@樣一段代碼

public void ipp() {
    int i = 1;
    ++i;
}

查看其字節(jié)碼

iconst_1
istore_1
iinc 1 1
return

發(fā)現(xiàn)沒,完全一樣。也就是說,在沒有賦值操作時(shí),i++ 和 ++i 編譯成字節(jié)碼后,都是

iinc 1 1

完全一樣

有多少人之前的理解是 i++ 和 ++i 本身孤零零地放在那是有區(qū)別的呢?

看 iinc 字節(jié)碼的定義

找到 JVM 官方手冊(cè)

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc

看到 iinc 字節(jié)碼指令的格式為

iinc index const

index 表示局部變量表的索引,const 表示將其值加上多少

所以上面的

iinc 1 1

就表示

將局部變量表索引為 1 位置的值,加上 1

局部變量表索引 0 位置處是 this,索引 1 位置處的值,是 int i = 1 這段代碼設(shè)置的,也就是 1。把這個(gè)值加 1,就變成了了 2

再來回顧下上面的代碼

public void ipp() {
    int i = 1;
    i++;
    System.out.println(i);
}

public void ppi() {
    int i = 1;
    ++i;
    System.out.println(i);
}

如果打印 i 的值,很容易知道,兩個(gè)都是 2

所以很簡單,i++ 和 ++i 本身在字節(jié)碼指令中的體現(xiàn)都是 iinc,就是單純把 i 所在的局部變量表那個(gè)位置的值,+1

稍稍復(fù)雜一點(diǎn)

我們把上面的代碼稍稍復(fù)雜一點(diǎn),++ 操作后,再重新賦值給 i

public void ipp() {
    int i = 1;
    i = i++;
}

public void ppi() {
    int i = 1;
    i = ++i;
}

你猜 i 的值分別是多少

別急,再次查看字節(jié)碼

void ipp() --> i = i++;

iconst_1
istore_1
iload_1
iinc 1 1
istore_1
return

void ppi() --> i = ++i;

iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return

這回看到不一樣了,但字節(jié)碼指令都相同,只是順序不同

i = i++ 就是 先 iload_1 再 iinc 1 1

i = ++i 就是 先 iinc 1 1 再 iload_1

所以也很簡單,i++ 和 ++i 只有在最終賦值給某變量時(shí)(實(shí)際上是因?yàn)閰⑴c了運(yùn)算,因?yàn)橹苯淤x值也是一種無運(yùn)算符號(hào)的運(yùn)算),字節(jié)碼指令是不同的,而且也只是順序上的不同。

那順序的不同會(huì)導(dǎo)致結(jié)果怎樣呢?下面我們通過觀察 虛擬機(jī)棧 來細(xì)化整個(gè)過程

觀察虛擬機(jī)棧中的變化

但你得先知道虛擬機(jī)棧是什么,也就得知道 JVM 的內(nèi)存劃分,這塊就不幫你復(fù)習(xí)了哈,直接上 ipp () 方法入虛擬機(jī)棧后,這個(gè)方法棧幀里的初始構(gòu)造

看 i = i++

下面一步步執(zhí)行 ipp () 方法的字節(jié)碼

iconst_1
istore_1
iload_1
iinc 1 1
istore_1
return

注意局部變量表 0 處表示的是 this,這里為了簡化沒有寫出,然后棧幀的“幀”字寫錯(cuò)啦,我就任性一下不改了哈

iconst_1:將立即數(shù) 1 壓棧

istore_1:操作數(shù)棧頂 -> 局部變量表 1 位置

iload_1:局部變量表 1 位置 -> 操作數(shù)棧頂

iinc 1 1:局部變量表 1 位置的值 +1

istore_1:操作數(shù)棧頂 -> 局部變量表 1 位置

所以,最后 i 的值,也就是局部變量表中 1 位置處的值,就是 1

我們用動(dòng)畫再演示一遍

你可以感受到,i = i++ 這種寫法,iinc 1 1 這一步是完全沒有用的,因?yàn)樽詈缶植孔兞勘?1 位置處的值,在最后一步賦值操作時(shí),會(huì)被操作數(shù)棧頂處的值覆蓋,所以之前的 +1 完全沒用

所以 idea 也會(huì)提示你,這里的 i++ 沒用

the value changed at 'i++' is never used

再看 i = ++i

相信這個(gè)你自己也可以推導(dǎo)出來了

iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return

iconst_1:將立即數(shù) 1 壓棧

istore_1:操作棧頂 -> 局部變量表 1 位置

iinc 1 1:局部變量表 1 位置的值 +1

iload_1:局部變量表 1 位置 -> 操作棧頂

istore_1:操作棧頂 -> 局部變量表 1 位置

所以,最后 i 的值,也就是局部變量表中 1 位置處的值,就是 2

我們直接用動(dòng)畫演示一遍

本質(zhì)區(qū)別

所以看出本質(zhì)區(qū)別是什么了么?

區(qū)別就是

是 "先把局部變量表中的值 +1,再放到操作數(shù)棧中"

還是 "先放到操作數(shù)棧中,再把局部變量表中的值 +1"

僅此而已

所以網(wǎng)上普遍的說法,i++ 表示 先 賦值運(yùn)算

賦值就是 壓入操作數(shù)棧頂

運(yùn)算就是 局部變量表 +1 操作

反正這倆詞我是對(duì)應(yīng)不上...

還有的說法是,i++ 是先把 i 拿出來使用,然后再 + 1;

還有的說法是,i++ 先賦值再自增

還有的 ... ...

哥哥誒,咱別用自己造的詞誤導(dǎo)讀者了好不?

所以最后用我的話總結(jié)一個(gè)沒有任何歧義的

i++:先將局部變量表中的 i 放入操作數(shù)棧中,再將局部變量表中的 i 值 +1

++i:先將局部變量表中的 i 值 +1,再將 i 放入操作數(shù)棧中

來點(diǎn)難的

當(dāng)你從這個(gè)角度理解了之后,再做類似的復(fù)雜一點(diǎn)的題,也不在話下,大不了在腦子里從頭推導(dǎo)一遍即可

看題

int i = 2;
int y = i++ + ++i;
y = ?

int a = 2;
a = a++ + ++a;
a = ?

int b = 2;
b = b++ + (++b + ++b) + (b += 2);
b = ?

答案

y = 6

a = 6

b = 18

你做對(duì)了么?

我把最難的那個(gè)題的字節(jié)碼展示出來

按照黃色的字可以到操作數(shù)棧的變化(從左到右就是操作數(shù)棧從棧底到棧頂),自己腦補(bǔ)一下動(dòng)畫吧,不想做了有點(diǎn)懶哈哈哈~

int b = 2;
b = b++ + (++b + ++b) + (b += 2);
iconst_2    ; 操作數(shù)棧 2
istore_1    ; 局部變量表 b=2
iload_1     ; 操作數(shù)棧 2
iinc 1 by 1 ; 局部變量表 b=3
iinc 1 by 1 ; 局部變量表 b=4
iload_1     ; 操作數(shù)棧 2 4
iinc 1 by 1 ; 局部變量表 b=5
iload_1     ; 操作數(shù)棧 2 4 5
iadd        ; 操作數(shù)棧 2 9(=4+5)
iadd        ; 操作數(shù)棧 11(=2+9)
iinc 1 by 2 ; 局部變量表 b=7
iload_1     ; 操作數(shù)棧 11 7
iadd        ; 操作數(shù)棧 18(=11+7)
istore_1    ; 局部變量表 b=18

再難的,我覺得就有些無聊了,大家自己給自己出題吧~

如果你對(duì)這里的入棧順序有困惑,比如你感覺加了()數(shù)學(xué)上不是先進(jìn)行運(yùn)算么?怎么不是先入棧參與運(yùn)算呢?

那其實(shí)這和 i++ 與 ++i 的知識(shí)就不相關(guān)了,你需要了解的是 前綴、中綴、后綴表達(dá)式,這里只舉個(gè)例子不展開講解。

簡單說就是如何將數(shù)學(xué)表達(dá)式,轉(zhuǎn)換成一種格式,按照這個(gè)順序可以方便通過棧來實(shí)現(xiàn)計(jì)算。

比如

b++ + (++b + ++b) + (b += 2)

在轉(zhuǎn)成后綴表達(dá)式過程中 ++ 操作根本不受影響,先簡化成

b + (b + b) + b

轉(zhuǎn)換成后綴表達(dá)式后就是

b b b + + b +

照著這個(gè)順序壓棧,就是字節(jié)碼中指令的順序啦,比如 b 壓棧就是 iload_1,運(yùn)算符(+)壓棧就是 iadd,你再回過去證明一下哦~

而這里的每一個(gè) b 的值,就是壓棧那一時(shí)刻的 b 的值

最后憤怒地再說兩句

所以,網(wǎng)上關(guān)于 ++ 的題目,其實(shí)是兩個(gè)知識(shí)點(diǎn)

  • i++ 與 ++i 參與運(yùn)算時(shí)的字節(jié)碼指令

  • 將數(shù)學(xué)表達(dá)式轉(zhuǎn)換為棧操作的后綴表達(dá)式

而網(wǎng)上的講解,大部分都不是從最直接的字節(jié)碼指令說,還將兩個(gè)知識(shí)點(diǎn)混為一談,我覺得是不負(fù)責(zé)任的。

回過頭來再看開頭說的那篇文章的結(jié)論

i++ 先賦值在運(yùn)算,例如 a=i++,先賦值 a=i,后運(yùn)算 i=i+1,所以結(jié)果是 a==1

++i 先運(yùn)算在賦值,例如 a=++i,先運(yùn)算 i=i+1,后賦值 a=i,所以結(jié)果是 a==2

先不說它沒有用字節(jié)碼來說明問題,你有沒有發(fā)現(xiàn)這說的本身就是錯(cuò)的,有很大的誤導(dǎo)性。

先賦值 a=i,后運(yùn)算 i=i+1

其實(shí)根本沒有先賦值吧,只是把 i 丟到操作數(shù)棧中等待被運(yùn)算而已,然后局部變量表 i=i+1,最后操作數(shù)棧中的 i 出棧并寫入局部變量表中 a 的位置,這時(shí)才叫賦值。

總之,這類文章還是少看為好

本文來自微信公眾號(hào):低并發(fā)編程 (ID:dibingfa),作者:閃客

廣告聲明:文內(nèi)含有的對(duì)外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。

相關(guān)文章

關(guān)鍵詞:java,編程

軟媒旗下網(wǎng)站: IT之家 最會(huì)買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機(jī)APP應(yīng)用 魔方 最會(huì)買 要知