深入理解Java中的i++、++i語句
在幾乎所有的指令式程式設計語言中,必然都會有i++和++i這種語法。在程式設計啟蒙教材《C語言程式設計》一書中,也專門解釋了這兩條語句的區別。有些語言中i++和++i既可以作為左值又可以作為右值,筆者專門測試了一下,在Java
語言中,這兩條語句都只能作為右值,而不能作為左值。同時,它們都可以作為獨立的一條指令執行。
int i = 0;
int j1 = i++; // 正確
int j2 = ++i; // 正確
i++; // 正確
++i; // 正確
i++ = 5; // 編譯不通過
++i = 5; // 編譯不通過
關於i++和++i的區別,稍微有經驗的程式設計師都或多或少都是瞭解的,為了文章的完整性,本文也通過例項來簡單地解釋一下。
{
int i = 1;
int j1 = i++;
System.out.println("j1=" + j1); // 輸出 j1=1
System.out.println("i=" + i); // 輸出 i=2
}
{
int i = 1;
int j2 = ++i;
System.out.println("j2=" + j2); // 輸出 j2=2
System.out.println("i=" + i); // 輸出 i=2
}
上面的例子中可以看到,無論是i++和++i指令,對於i
變數本身來說是沒有任何區別,指令執行的結果都是i變數的值加1。而對於j1和j2來說,這就是區別所在。
int i = 1;
int j1 = i++; // 先將i的原始值(1)賦值給變數j1(1),然後i變數的值加1
int j1 = ++i; // 先將i變數的值加1,然後將i的當前值(2)賦值給變數j1(2)
上面的內容是程式設計基礎,是程式設計師必須要掌握的知識點。本文將在此基礎上更加深入地研究其實現原理和陷阱,也有一定的深度。在讀本文之前,您應該瞭解:
- 多執行緒相關知識
- Java編譯相關知識
- JMM(Java記憶體模型)
本文接下來的主要內容包括:
- Java中i++和++i的實現原理
- 在使用i++和++i時可能會遇到的一些“坑”
i++和++i的實現原理
接下來讓我們深入到編譯後的位元組碼層面上來了解i++和++i的實現原理,為了方便對比,筆者將這兩個指令分別放在2個不同的方法中執行,原始碼如下:
public class Test {
public void testIPlus() {
int i = 0;
int j = i++;
}
public void testPlusI() {
int i = 0;
int j = ++i;
}
}
將上面的原始碼編譯之後,使用javap
命令檢視編譯生成的程式碼(忽略次要程式碼)如下:
...
{
...
public void testIPlus();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0 // 生成整數0
1: istore_1 // 將整數0賦值給1號儲存單元(即變數i)
2: iload_1 // 將1號儲存單元的值載入到資料棧(此時 i=0,棧頂值為0)
3: iinc 1, 1 // 1號儲存單元的值+1(此時 i=1)
6: istore_2 // 將資料棧頂的值(0)取出來賦值給2號儲存單元(即變數j,此時i=1,j=0)
7: return // 返回時:i=1,j=0
LineNumberTable:
line 4: 0
line 5: 2
line 6: 7
public void testPlusI();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0 // 生成整數0
1: istore_1 // 將整數0賦值給1號儲存單元(即變數i)
2: iinc 1, 1 // 1號儲存單元的值+1(此時 i=1)
5: iload_1 // 將1號儲存單元的值載入到資料棧(此時 i=1,棧頂值為1)
6: istore_2 // 將資料棧頂的值(1)取出來賦值給2號儲存單元(即變數j,此時i=1,j=1)
7: return // 返回時:i=1,j=1
LineNumberTable:
line 9: 0
line 10: 2
line 11: 7
}
...
i++和++i在使用時的一些坑
i++和++i在一些特殊場景下可能會產生意想不到的結果,本節介紹兩種會導致結果混亂的使用場景,並剖析其原因。
i = i++的導致的結果“異常”
首先來看一下下面程式碼執行後的結果。
int i = 0;
i = i++;
System.out.println("i=" + i); // 輸出 i=0
正常來講,執行的結果應該是:i=1
,實際結果卻是:
i=0
,這多少會讓人有些詫異。為什麼會出現這種情況呢?我們來從編碼後的程式碼中找答案。上面的程式碼編譯後的核心程式碼如下:
0: iconst_0 // 生成整數0
1: istore_1 // 將整數0賦值給1號儲存單元(即變數i,i=0)
2: iload_1 // 將1號儲存單元的值載入到資料棧(此時 i=0,棧頂值為0)
3: iinc 1, 1 // 號儲存單元的值+1(此時 i=1)
6: istore_1 // 將資料棧頂的值(0)取出來賦值給1號儲存單元(即變數i,此時i=0)
7: getstatic #16 // 下面是列印到控制檯指令
10: new #22
13: dup
14: ldc #24
16: invokespecial #26
19: iload_1
20: invokevirtual #29
23: invokevirtual #33
26: invokevirtual #37
29: return
從編碼指令可以看出,i
被棧頂值所覆蓋,導致最終i
的值仍然是i
的初始值。無論重複多少次i = i++
操作,最終i的值都是其初始值。
i++
會產生這樣的結果,那麼++i
又會是怎樣呢?同樣的程式碼順序,將i++
替換成++i
如下:
int i = 0;
i = ++i; // IDE丟擲【The assignment to variable i has no effect】警告
System.out.println("i=" + i); // 輸出i=1
可以看到,使用++i
時出現了“正確”的結果,同時Eclipse IDE中丟擲【The assignment to variable i has no effect】警告,警告的意思是將值賦給變數i毫無作用,並不會改變i的值。也就是說:i = ++i
等價於++i
。
多執行緒併發引發的混亂
先來看看之前部落格中的一個例子,例子中展示了在多執行緒環境下由++i
操作引起的資料混亂。引發混亂的原因是:++i
操作不是原子操作。
雖然在Java
中++i
是一條語句,位元組碼層面上也是對應iinc
這條JVM指令,但是從最底層的CPU層面上來說,
++i
操作大致可以分解為以下3個指令:
- 取數
- 累加
- 儲存
其中的一條指令可以保證是原子操作,但是3條指令合在一起卻不是,這就導致了++i
語句不是原子操作。
如果變數i
用volatile
修飾是否可以保證++i
是原子操作呢,實際上這也是不行的。至於原因,以後會專門寫文章介紹volatile
等關鍵詞的意義。如果要保證累加操作的原子性,可以採取下面的方法:
- 將
++i
置於同步塊中,可以是synchronized
或者J.U.C中的排他鎖(如ReentrantLock等)。 - 使用原子性(Atomic)類替換
++i
,具體使用哪個類由變數型別決定。如果i
是整形,則使用AtomicInteger
類,其中的AtomicInteger#addAndGet()
就對應著++i
語句,不過它是原子性操作。