前言:眾所周知,
i++
和++i
的區(qū)別是:i++
先將i
的值賦值給變量,再將i
的值自增1;而++i
則是先將i
的值自增1,再將結(jié)果賦值給變量。因此,二者最終都給i
自增了1,只是方式不同而已。當然,如果在面試過程中面試官問你這個問題,只回答出上述內(nèi)容,只能說明你對這方面的知識了解的還是太淺顯。那么
i++
和++i
到底有什么不同之處呢?
一、局部變量表與操作數(shù)棧簡介
《深入理解Java虛擬機》第八章對棧幀結(jié)構(gòu)有如下描述Java虛擬機以方法作為最基本的執(zhí)行單元,“棧幀”(Stack Frame)則是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)結(jié)構(gòu),它也是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧的棧元素。
在一個活動線程中,可能會執(zhí)行多個方法,因此會存在多個棧幀,和“棧”(先進后出)一樣,處于棧頂?shù)臈攀钦嬲\行的,處于棧頂?shù)臈Q作“當前棧幀”(Current Stack Frame),這個棧幀所屬的方法稱作“當前方法”(Current Method)。
在執(zhí)行main
方法時,main
方法所屬的線程主線程,假設(shè)在主線程中調(diào)用了一個method1()
方法,在method1()
內(nèi)部調(diào)用了method2()
方法,在method2()
方法執(zhí)行兩個整數(shù)運算,示例如下:
/**
* 方法調(diào)用
*
* @author iCode504
* @date 2023-10-23 22:05
*/
public class StackFrameDemo1 {
public static void main(String[] args) {
System.out.println("main開始執(zhí)行");
method1();
System.out.println("main執(zhí)行完成");
}
private static void method1() {
System.out.println("method1開始執(zhí)行");
int result = method2();
System.out.println("result = " + result);
System.out.println("method1執(zhí)行結(jié)束");
}
private static int method2() {
int var1 = 10;
int var2 = 20;
return var1 + var2;
}
}
運行結(jié)果:
由代碼我們可以看出,main
方法最先執(zhí)行一個輸出,然后進入method1
執(zhí)行第一個輸出,再完整執(zhí)行method2
。method2
執(zhí)行完成以后,再執(zhí)行method1
,最后執(zhí)行main
方法,由于這段代碼中只涉及一個主線程,并且最先完整執(zhí)行方法的是method2
,因此method2
對應(yīng)的棧幀就是當前棧幀,main
方法最后執(zhí)行完畢,因此main
方法對應(yīng)的棧幀在method2
和method1
之下。以下是這段代碼對應(yīng)的棧幀概念圖:
在每一個棧幀中存儲了方法的局部變量表、操作數(shù)棧、動態(tài)鏈接和方法返回地址等信息
1.1 局部變量表
局部變量表(Local variable Table)是一組變量值的存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。
局部變量表的容量是以變量槽(Variable Slot)為最小單位,每個變量槽能存儲基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的數(shù)據(jù)。為了盡可能節(jié)省棧幀消耗的內(nèi)存空間,局部變量表中的變量槽是可以重用的。
JVM使用索引定位的方式使用索引變量表,索引值的范圍是從0開始到局部變量表最大變量槽的數(shù)量(類似數(shù)組結(jié)構(gòu))。
當一個方法被調(diào)用的時候,JVM會使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞,即實參到形參的傳遞。
1.2 操作數(shù)棧
操作數(shù)棧(Operand Stack)也稱作操作數(shù)棧,它是一個棧結(jié)構(gòu)(后進先出,例如手槍的彈夾,先打出去的子彈是最頂上的子彈)。
在方法開始執(zhí)行的時候,這個方法對應(yīng)的操作數(shù)棧是空的,在方法執(zhí)行過程中,會有各種字節(jié)碼指令向操作數(shù)棧中寫入或讀取內(nèi)容,即出棧和入棧操作,例如:兩數(shù)相加運算時,就需要將兩個數(shù)壓入棧頂后調(diào)用運算指令。
操作數(shù)棧中的元素的數(shù)據(jù)類型必須和字節(jié)碼指令序列嚴格匹配,在編譯程序代碼的時候編譯器必須要嚴格保證這一點,在類的校驗階段的數(shù)據(jù)流分析時候還需要再次校驗。例如:執(zhí)行加法iadd
(i
是int
類型,add
是兩個數(shù)相加)命令時,就需要保證兩個操作數(shù)必須是int
類型,不能出現(xiàn)其他類型相加的情況。
二、字節(jié)碼分析(圖解)
我們可以從字節(jié)碼的角度進一步對i++
和++i
的執(zhí)行過程做進一步的分析。以下面代碼為例:
/**
* i++和++i的深入分析
*
* @author iCode504
* @date 2023-10-17 5:58
*/
public class IncrementAndDecrementOperators2 {
public static void main(String[] args) {
int intValue1 = 2;
int intValue2 = 2;
int result1 = intValue1++;
int result2 = ++intValue2;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
我們需要查看編譯后的字節(jié)碼文件,字節(jié)碼文件不能直接使用記事本打開,但是我們可以使用javap -verbose 文件名.class
命令,以IncrementAndDecrementOperators2.class
為例:
javap -verbose IncrementAndDecrementOperators2.class
此時就會打開所有的字節(jié)碼文件,我們只需要關(guān)注main
方法內(nèi)的執(zhí)行過程即可:
首先來解釋一下這四行代碼的含義:
0: iconst_2
1: istore_1
2: iconst_2
3: istore_2
-
iconst_2
一共有兩部分組成,i
指的是int
類型(源代碼中我們定義的確實是int
類型),const
代表常量(數(shù)字2
是整型常量),iconst_2
的含義是將2入操作數(shù)棧。 -
istore_1
中的store
代表的是存儲,istore_1
的含義是將操作數(shù)棧中的數(shù)值2出棧,存入到局部變量表1的位置。同理,i_store2
表示將操作數(shù)棧中的數(shù)值2出棧,存儲到局部變量表2的位置。
以下是前面四行代碼存儲過程圖(存儲過程全部流程圖點擊此鏈接下載:點我下載):
此時我們繼續(xù)觀察4-8行代碼:
4: iload_1
5: iinc 1, 1
8: istore_3
-
iload_1
的作用是將局部變量表1號位置存儲的值移動到操作數(shù)棧的棧頂。 - 第5行的
iinc
有兩個參數(shù),第一個參數(shù)1
是局部變量表的位置,另一個參數(shù)1
的含義是在該位置存儲一個1
,如果這個位置存在值,那么這個值的結(jié)果是已存在值 + 參數(shù)值。 -
istore_3
將操作數(shù)棧中的數(shù)移動到局部變量表的3號位置。
以下是這三行代碼的示意圖:
9-12行的字節(jié)碼的作用原理和4-8行的作用原理基本相同:
9: iinc 2, 1
12: iload_2
13: istore 4
istore 4
的作用是將操作數(shù)棧中的值存儲到局部變量表4號位置。
以下是這三行代碼的示意圖:
接下來15-30行是和系統(tǒng)輸出有關(guān)的。其中第30行iload_3
在局部變量表中(這個值為2)值移動到操作數(shù)棧頂供系統(tǒng)輸出,事實上iload_3
的值正好對應(yīng)源代碼中變量result1
的值。也就是說,result1
輸出結(jié)果就是iload_3
的數(shù)值2。
同理,iload 4
就是第二個要輸出的值,在局部變量表中第4個位置存儲的值正好是3,而輸出的變量名是result2
,因此result2
的輸出結(jié)果是3。
三、i++
和++i
性能分析
i++
和++i
主要用在普通for
循環(huán)上,那么我們就將二者用在for
循環(huán)上,循環(huán)相同的次數(shù),從字節(jié)碼的角度進行分析。
以下是使用i++
和++i
的兩個for
循環(huán)文件:
/**
* i++在for循環(huán)的使用
*
* @author ZhaoCong
* @date 2023-10-21 16:14:33
*/
public class LoopTest1 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
}
}
}
/**
* ++i在for循環(huán)的使用
*
* @author ZhaoCong
* @date 2023-10-21 16:15:17
*/
public class LoopTest2 {
public static void main(String[] args) {
for (int i = 0; i < 100; ++i) {
}
}
}
執(zhí)行編譯命令以后,我們來查看兩個文件的字節(jié)碼:
仔細觀察這兩個字節(jié)碼文件內(nèi)容,我們發(fā)現(xiàn)在兩個文件main
方法的字節(jié)碼內(nèi)容完全相同。由此可見,兩種方式執(zhí)行for
循環(huán)的效率是相同的。