Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!

同很多高級編程語言一樣,Java語言的運算符系統當中也有自增(++)和自減(--)這兩個運算符。很多小夥伴對這兩個運算符都深感頭疼,並且很多公司在面試的時候也經常會問到與之相關的問題,今天我們就通過一篇文章來深度解析一下這兩個運算符,相信在看過這篇文章之後,你再也不會被自增(++)和自減(--)運算符難住。由於自增和自減運算符的原理完全相同,所以我們在講解的時候僅以自增運算符舉例。(預警:本文舉例較多,篇幅較長,請耐心看完)

一、基本運算規則介紹

自增(++)和自減(--)運算符是對變量在原始值的基礎上進行加1或減1的操作。它們都有前綴和後綴兩種形式。前綴形式的運算規則可以概括爲:”先自增(減),後引用”,而後綴形式的運算規則可以概括爲:”先引用,後自增(減)”。這裏所說的”引用”,指的是使用變量的值。另外,我們還要強調一個細節:無論是前綴形式還是後綴形式,自增自減運算符的優先級要高於賦值運算符。大家要記清楚這個細節,後文還會針對這個細節進行論述。下面我們就分爲幾種情況來研究++和--在不同場合下的運算效果。

二、語句中僅有++或--

請看代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
我們可以看到,這段代碼中總共有3條語句,其中第2條語句中僅有一個後綴形式++操作,程序的輸出結果是3。那麼我們再來看另外一段代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這段代碼與之前的那段代碼基本一樣,只是第2條語句中,後綴形式的++操作被換成了前綴形式,程序的輸出結果還是3。這說明:當一條語句中僅有++或--操作時,前綴形式與後綴形式的運算符沒有任何區別。請注意:這句話的前半句是一個很重要的前提,那就是” 一條語句中僅有++或--操作”,如果脫離了這個前提,後半句所說的結論並不成立。

三、++或--運算結果賦值給其他變量

可能有些小夥伴沒看明白這個標題是什麼意思,我們來看一段代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這段代碼的第2條語句對變量a進行了自增操作,並且把這個操作結果賦值給另一個變量b。語句中的變量b就是標題中所說的”其他變量”,是指沒有進行自增自減操作的其他變量。爲什麼要強調”賦值給其他變量”這個前提呢?就是因爲如果把運算結果賦值給變量a自身,又會產生不同的效果,我們後面再去講解賦值給自身的情況。現在先來分析程序,重點看第2條語句:變量a所進行的是前綴形式的自增操作,那麼按照”先自增後引用”的運算規則,a的值首先變成3,然後賦值b。因此,給變量b賦值的是3,那麼輸出結果當然就是3和3。
如果代碼變成下面的樣子:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這一次,代碼的第2行發生了變化,a的自增操作變成了後綴形式。此時的程序輸出結果是3和2。爲什麼會是這樣的運行結果呢?網上有很多資料對此的解釋是:因爲表達式中出現的是後綴形式的自增操作,因此,運算的規則就變成了”先引用後自增”。計算機會先使用a的值給b賦值,a的值是2,所以b被賦值爲2,a在完成給b賦值的操作之後,纔會完成自增變爲3,所以程序的輸出結果爲3和2。這種解釋看似非常合理,但其實是錯誤的!

按照這種解釋,後綴形式的自增是在賦值之後才完成的,由此可以推出後綴形式的自增自減運算的優先級比賦值運算的優先級更低。而我們之前已經特意強調過:無論是前綴形式還是後綴形式,自增自減運算符的優先級都比賦值運算符要高。接下來問題來了:既然++和--的運算優先級高於賦值運算符,那麼爲什麼賦值之前a的值沒有自增爲3呢?

其實這是個錯覺!a在賦值給變量b之前,就已經完成了自增!爲了講解清楚真實情況,我們必須科普一個小常識,那就是:當程序中,如果變量參與了算術運算、或者以變量的值進行賦值,或者打印了某個變量的值,總之只要程序中用到這個變量的值,都會先把這個變量存入一個臨時的空間,專業上把這個臨時的空間稱之爲”操作數棧”。我們之前所說的”先引用後自增”中所說的這個”引用”操作,其實就是指”把變量的值存入操作數棧”這個動作。當程序中需要用到變量的值,計算機是從”操作數棧”中取出值進行運算,並不是我們想象的直接從變量所在的內存單元中取出數值。但是,如果語句中僅有++或--,並不會把變量的值存入操作數棧,而是直接對變量進行自增或自減的操作,這也是爲什麼我們把語句中僅有++或--單獨作爲一種情況講解的原因。

科普完這個小常識之後,我們來解釋剛纔的代碼爲什麼會輸出3和2。代碼中出現用a的值給變量b賦值的語句,並且a的後面出現了++,說明要對a進行後綴形式的自增操作。按照我們剛纔科普的小常識,a參與了賦值運算,那麼就會把a的值存入操作數棧。因爲a的自增是後綴形式的,所以要遵循”先引用後自增”的運算規則,因此,計算機會首先取出a的值2存入操作數棧,然後再把a的值增加到3。做完自增操作之後,接下來會對變量b進行賦值操作。那麼,是用哪個值給變量b賦值呢?就是用剛纔存到操作數棧中的那個2對變量b進行賦值,所以b最終得到的值是2,因此輸出結果是3和2。
在這裏,請大家注意一個細節,那就是:代碼中的自增操作雖然是後綴形式的,但這個自增動作卻是在賦值之前完成的,這也解釋了後綴形式的自增運算優先級高於賦值運算,而網上很多資料中所說的”先完成賦值再去做自增操作”是完全錯誤的。
那麼,之前例題中第2條語句是”b=++a;”,會不會也把a的值存入操作數棧呢?答案是肯定的,只要是變量參了算術運算、賦值、被打印這些操作,都會取出變量的值存入操作數棧。因爲語句中出現的是前綴形式的自增,所以在把值存入操作數棧之前就已經完成了自增操作。

好,現在我們來加大一點難度,請看代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這段代碼的第2條語句,出現了2次++a,那麼運算結果會是多少呢?”=”右邊的表達式爲”++a + ++a”,按照運算優先級,計算機首先對a進行自增操作,經過這一步操作之後,變量a的值變成了3,緊接着,計算機會把這個3存入操作數棧中,爲了方便講述,我們把這個操作數棧叫做”棧1”,接下來,表達式中又出現了++a,此時計算機再次對a進行自增,a的值變成了4,爲了完成加法操作,計算機又把這個4存入到另一個操作數棧中。我們把這個操作數棧稱爲”棧2”。按照運算優先級,接下來要做的事情就是完成加法運算,計算機把”棧1”中的3和”棧2”中的4分別取出並且進行相加,得到的結果是7,最後把這個7賦值給”=”左邊的變量b。所以上面代碼運行所輸出的結果分別是4和7。

下面我們再來研究一個關於運算順序問題,請看下面的代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
我們還是重點來分析第2行代碼。”=”右邊首先是++a,所以對先a進行自增操作,此時a變成了3,緊接着把自增之後的值存入”棧1”,接下來遇到了a++,因爲是後綴形式的自增操作,遵循”先引用後自增”的運算規則,計算機先取出a的值3,並且存入”棧2”,然後對a進行自增的操作,a的值變成了4。現在,”棧1”和”棧2”中都已經存入了值,那麼,此時計算機是先把”棧1”和”棧2”中的值做個相加操作呢,還是先去做變量a的第3次自增操作呢?很多同學肯定都會認爲計算機肯定是先去完成a的第3次自增操作,理由是自增自減運算優先級高於加法運算。其實不然,當運算進行到這一步的時候,計算機會先把”棧1”和”棧2”中的兩個值相加,然後纔去完成a的第3次自增操作。很多人都不理解爲什麼會這樣,難道不是++a的優先級更高嗎?

這裏需要澄清一個大家對”優先級”概念的誤解。當計算機有AB兩個操作可以做的時候,如果選擇先完成A得到的結果是X,而選擇先完成B得到結果是Y,此時計算機必須按照運算的優先級做出選擇。而如果先完成A和先完成B對運算結果沒有影響,那麼計算機就會先完成左邊(先出現的)的操作。此時,如果把”棧1”和”棧2”中的值相加,並不會影響到最終的運算結果,並且這個操作是先於”a的第3次自增”出現的,所以計算機會先把”棧1”和”棧2”中的值加起來,然後才完成”a的第3次自增”。

很多小夥伴可能不理解,那麼自增操作優先級高於加法操作,是如何體現出來的呢?大家仔細看,在代碼的第2條語句中,a的自增出現了3次,加法運算出現了2次。”a的第2次自增”後於”第1次加法運算”出現,但卻先於”第1次加法運算”執行,同理,”a的第3次自增”後於”第2次加法運算”出現,卻先於”第2次加法運算”執行。這些都體現出自增運算的優先級是高於加法運算的。

講清楚”優先級”這個問題之後,我們再回到例題本身。第1次加法運算是3和3相加,結果是6,計算機會把這個6存入”棧3”。緊接着計算機看到表達式中第2次出現了++a,於是再次對a進行自增操作,a的值變成了5,並且存入”棧4”。之後就是完成”棧3”和”棧4”中數值的相加操作,也就是把6和5相加,最終得到的結果是11。因此程序最終的輸出結果是5和11。

四、++或--運算結果賦值給自身

標題中所說的”自身”,就是指進行了自增或自減運算操作的變量。我們來看下面的代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
以上代碼的第2條語句,對a進行了前綴形式的自增,然後又賦值給a自身,那麼a的值是多少呢?因爲a進行的是前綴形式的自增,所以運算規則是”先自增後引用”,自增之後a的值變成了3,把3存入操作數棧,之後以3賦值給a,所以a的值還是3。

但是,如果我們把代碼變成如下形式:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這一次,我們把第2條語句中的++a改成了a++。這種情況下,程序輸出a的值爲竟然爲2,而不是3。說的直白一點,a並沒有按我們的想象實現自增。這是爲什麼呢?我們來分析一下整個運算的過程:計算機看到”=”右邊是後綴形式的自增,因此以”先引用後自增”的規則進行運算,先把a的值存入操作數棧,緊接着對a進行自增操作,a的值變成了3,最後又用操作數棧中的那個2對a進行賦值,a的值又變成了2。這樣給我們造成了一種”a沒有進行自增”的錯覺。之前說過,網上很多資料都誤傳“後綴形式自增操作優先級低於賦值運算”。如果按照這種錯誤說法,無法解釋以上代碼最終的輸出結果爲2的原因。這也是爲什麼本文一開始就強調”無論是前綴形式還是後綴形式,自增自減運算符的優先級要高於賦值運算符”的原因,就是因爲這個細節是破解此類問題的關鍵點。類似的情況還可以衍生出很多版本,例如以下這段代碼::

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這段代碼中,第2條語句對a進行了兩次自增操作,最終輸出a的值是6。而如果按照”後綴形式的自增自減優先級低於賦值運算”的錯誤說法,則會認爲最終輸出a的值是7,理由是”=”右邊的運算結果是6,賦值給”=”左邊的a之後,又進行一次自增,最終的結果是7,當然,事實可以證明這種理解是錯誤的。
接下來,我們再來研究一種更特殊的情況,請看以下代碼:

Java千問:學透Java自增(++)自減(--)運算符,看這一篇就夠了!
這一次,語句中出現了複合賦值運算符。如果程序運行,輸出a的值會是多少呢?我們首先可以推導出”+=”右邊的運算結果是7(具體推導過程不再贅述)。我們還知道,複合賦值運算符在完成運算的時候,要把右邊當作整體。那麼現在關鍵的問題就只剩一個了,那就是:”+=”左邊的a到底是多少?很多人認爲”+=”左邊a的值應該是4,原因是++的運算優先級高於+=,所以要先完成2次自增,完成了2次自增以後,a的值已經變成了4,由此推得”+=”左邊a的值應該是4,而最終的運算結果是11(4+7的和)。但實際運行程序的話,可以看到輸出a的值爲9而非11。這是爲什麼呢?就是因爲+=的優先級雖然低於++,但是計算機在實際完成+=運算的時候會分爲好幾個步驟進行。我們可以大致把+=運算分解爲四大步驟:
A、把+=左邊的變量值存入操作數棧1
B、計算+=右邊的表達式,並把計算結果存入操作數棧2(此步驟其實是由多個具體步驟組成的)
C、把操作數棧1和操作數棧2中的數值相加得到運算結果
D、把運算結果存入變量a當中

現在最關鍵的問題是步驟A和B哪一個先被執行。如果先執行步驟A,那麼存入操作數棧1的是變量a自增之前的值,也就是2;反之,如果先執行B,那麼存入操作數棧1的是變量a自增之後的值,也就是4。真實的情況是先執行步驟A,也就是把變量a自增之前的值存入操作數棧1。這是一個普遍適用的規律,所以大家一定要記住:當語句中以複合賦值運算符給變量賦值的時候,計算機會先把複合賦值運算符左邊變量的值存入操作數棧。因此,這段程序運行的結果是9。

五、總結與說明

1、以上,我們把自增自減運算符的題目總結爲3大類,無論是企業的面試題,還是學校的考試題,關於自增自減運算符的題目基本都可以歸結到這3大類中。所以,仔細研究這3大類題目的規律,基本可以保證相關題目不會難住你。
2、筆者爲確保對文中所提到的代碼都能做到正確解釋,對文中所有代碼均用javap命令查看了編譯後的字節碼。
3、文中例題中的代碼如果放到C語言環境下,執行結果會有不同,原因是C語言對源碼的編譯和解析規則與Java語言略有不同,因此,千萬不要把本文的結論套用到C語言中。
4、雖然文章詳細分析了自增自減運算的各種情況,但筆者本身非常反對代碼中出現類似於” a += ++a + ++a;”這樣的代碼,因爲這樣的代碼可讀性非常差,很容易造成誤解,所以,各位小夥伴在真實編程的時候,一定別怕麻煩,哪怕多寫幾行代碼,也要把代碼寫的可讀性強一些,儘量不要讓人產生誤解和歧義。
5、爲方便初學者理解,文中並沒有出現編譯器、解析器之類的名詞。而文中所提到的”棧1”、 ”棧2”、 ”棧3”、 ”棧4”這些操作數棧的名稱,也只是爲了方便讀者理解而起的名稱,並非對應系統真實對各個操作數棧的命名。

希望本文能夠幫助初學者深入理解Java語言自增自減運算符。

如想系統學習Java編程,歡迎觀看我在本站的視頻課程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章