面試官又整新活,居然問(wèn)我 For 循環(huán)用 i++ 和 ++i 哪個(gè)效率高?
前幾天,一個(gè)小伙伴告訴我,他在面試的時(shí)候被面試官問(wèn)了這么一個(gè)問(wèn)題:
在for循環(huán)中,到底應(yīng)該用 i++ 還是 ++i ?
聽(tīng)到這,我感覺(jué)這面試官確實(shí)有點(diǎn)不按套路出牌了,放著好好的八股文不問(wèn),凈整些幺蛾子的東西。在臨走的時(shí)候,小伙伴問(wèn)面試官這道題的答案是什么,面試官?zèng)]有明確告訴答案,只是說(shuō)讓從程序執(zhí)行的效率角度自己思考一下。
好吧,既然這個(gè)問(wèn)題被拋了出來(lái),那我們就見(jiàn)招拆招,也給以后面試的小伙伴們排一下坑。
思路
前面提到,這個(gè)搞事情的面試官說(shuō)要從執(zhí)行效率的角度思考,那我們就拋開(kāi)語(yǔ)義上的區(qū)別,從運(yùn)行結(jié)果以外的效率來(lái)找找線索?;叵胍幌?,我們?cè)谝郧敖榻BCAS的文章中提到過(guò),后置自增i++和前置自增++i都不是原子操作,那么實(shí)際在執(zhí)行過(guò)程中是什么樣的呢?下面,我們從字節(jié)碼指令的角度,從底層進(jìn)行一波分析。
i++ 執(zhí)行過(guò)程
先寫(xiě)一段簡(jiǎn)單的代碼,核心功能就只有賦值和自增操作:
- public static void main(String[] args) {
- int i=3;
- int j=i++;
- System.out.println(j);
- }
下面用javap對(duì)字節(jié)碼文件進(jìn)行反編譯,看一下實(shí)際執(zhí)行的字節(jié)碼指令:
是不是有點(diǎn)難懂?沒(méi)關(guān)系,接下來(lái)我們用圖解的形式來(lái)直觀地看看具體執(zhí)行的過(guò)程,也幫大家解釋一下晦澀的字節(jié)碼指令是如何操作棧幀中的數(shù)據(jù)結(jié)構(gòu)的,為了簡(jiǎn)潔起見(jiàn),在圖中只列出棧幀中比較重要的操作數(shù)棧和局部變量表。
上面的代碼中除去打印語(yǔ)句,整體可以拆分成兩步,我們先看第一步 int i=3 是如何執(zhí)行的 。
上面兩條操作數(shù)棧和局部變量表相關(guān)的字節(jié)碼指令還是比較容易理解的,下面再看一下第二步int j=i++的執(zhí)行過(guò)程:
在上圖中需要注意的是,iinc能夠直接更新局部變量表中的變量值,它不需要把數(shù)值壓到操作數(shù)棧中就能夠直接進(jìn)行操作。在上面的過(guò)程中,拋去賦值等其他操作,i++實(shí)際執(zhí)行的字節(jié)碼指令是:
- 2: iload_1
- 3: iinc 1, 1
如果把它翻譯成我們能看懂的java代碼,可以理解為:
- int temp=i;
- i=i+1;
也就是說(shuō)在這個(gè)過(guò)程中,除了必須的自增操作以外,又引入了一個(gè)新的局部變量,接下來(lái)我們?cè)倏纯?+i的執(zhí)行過(guò)程。
++i 執(zhí)行過(guò)程
我們對(duì)上面的代碼做一點(diǎn)小小的改動(dòng),僅把i++換成++i,再來(lái)分析一下++i的執(zhí)行過(guò)程是怎樣的。
- public static void main(String[] args) {
- int i=3;
- int j=++i;
- System.out.println(j);
- }
同樣,用javap反編譯字節(jié)碼文件:
int i=3對(duì)應(yīng)前兩行字節(jié)碼指令,執(zhí)行過(guò)程和前面i++例子中完全相同,可以忽略不計(jì),重點(diǎn)還是通過(guò)圖解的方式看一下int j=++i對(duì)應(yīng)的字節(jié)碼指令的執(zhí)行過(guò)程:
拋去賦值操作,++i實(shí)際執(zhí)行過(guò)程只有一行字節(jié)碼指令:
- 2: iinc 1, 1
轉(zhuǎn)換成能理解的java代碼的話,++i實(shí)際執(zhí)行的就在局部變量中執(zhí)行的:
- i=i+1;
這么看來(lái),在使用++i時(shí)確實(shí)比i++少了一步操作,少引入了一個(gè)局部變量,如果在運(yùn)算結(jié)果相同的場(chǎng)景下,使用++i的話的確效率會(huì)比i++高那么一點(diǎn)點(diǎn)。
那么回到開(kāi)頭的問(wèn)題,兩種自增方式應(yīng)用在for循環(huán)中執(zhí)行的時(shí)候,那種效率更高呢?剛才得出的結(jié)論仍然適用于for循環(huán)中嗎,別急,讓我們接著往下看。
for循環(huán)中的自增
下面準(zhǔn)備兩段包含了for循環(huán)的代碼,分別使用i++后置自增和++i前置自增:
- //i++ 后置自增
- public class ForIpp {
- public static void main(String[] args) {
- for (int i = 0; i < 5; i++) {
- System.out.println(i);
- }
- }
- }
- //++i 前置自增
- public class ForPpi {
- public static void main(String[] args) {
- for (int i = 0; i < 5; ++i) {
- System.out.println(i);
- }
- }
- }
老規(guī)矩,還是直接反編譯后的字節(jié)碼文件,然后對(duì)比一下指令的執(zhí)行過(guò)程:
到這里,有趣的現(xiàn)象出現(xiàn)了,兩段程序執(zhí)行的字節(jié)碼指令部分居然一模一樣。先不考慮為什么會(huì)有這種現(xiàn)象,我們還是通過(guò)圖解來(lái)看一下字節(jié)碼指令的執(zhí)行過(guò)程:
可以清晰的看到,在進(jìn)行自增時(shí),都是直接執(zhí)行的iinc,在之前并沒(méi)有執(zhí)行iload的過(guò)程,也就是說(shuō),兩段代碼執(zhí)行的都是++i。這一過(guò)程的驗(yàn)證其實(shí)還有更簡(jiǎn)單的方法,直接使用idea打開(kāi)字節(jié)碼文件,就可以看到最終for循環(huán)中使用的相同的前置自增方式。
那么,為什么會(huì)出現(xiàn)這種現(xiàn)象呢?歸根結(jié)底,還是java編譯器對(duì)于代碼的優(yōu)化,在兩種自增方式中,如果沒(méi)有賦值操作,那么都會(huì)被優(yōu)化成一種方式,就像下面的兩個(gè)方法的代碼:
- void ipp(){
- int i=3;
- i++;
- }
- void ppi(){
- int i=3;
- ++i;
- }
最終執(zhí)行時(shí)的字節(jié)碼指令都是:
- 0: iconst_3
- 1: istore_1
- 2: iinc 1, 1
- 5: return
可以看到,在上面的這種特定情況下,代碼經(jīng)過(guò)編譯器的優(yōu)化,保持了語(yǔ)義不變,并通過(guò)轉(zhuǎn)換語(yǔ)法的形式提高了代碼的運(yùn)行效率。所以再回到我們開(kāi)頭的問(wèn)題,就可以得出結(jié)論,在for循環(huán)中,通過(guò)jvm進(jìn)行編譯優(yōu)化后,不論是i++還是++i,最終執(zhí)行的方式都是++i,因此執(zhí)行效率是相同的。
所以,以后再碰到這種半吊子的面試官,和你談for循環(huán)中i++和++i的效率問(wèn)題,自信點(diǎn),直接把答案甩在他的臉上,兩種方式效率一樣!
本文代碼基于Java 1.8.0_261-b12 版本測(cè)試