從位元組碼看Java中for-each迴圈(增強for迴圈)實現原理
阿新 • • 發佈:2019-01-31
下面是的兩個很簡單的類,可以看出它們的功能是一樣的。Java環境使用的是jdk1.8_111。
二、Collection的for-each 還是先貼兩段簡單的對比的程式碼,程式碼邏輯一樣。Java環境使用的是jdk1.8_111。
這兩段位元組碼中自帶的註釋很多,基本上看得懂,就不添加註釋了。 兩段位元組碼除了幾個變數儲存線上程的當前棧幀的區域性變量表的索引(astore_n,這個n就是索引)不一樣外,其餘的都是一模一樣的。不排除某次編譯後連那幾個索引值也一樣,那就真一模一樣了。位元組碼自帶的註釋都說了,Collection的for-each底層也是使用迭代器來實現的,兩種方式可以說是完全等價的。 對於實現了RandomAccess介面的實現類,因為它們的隨機訪問操作的時間複雜度為O(1),大多數情況使用傳統for迴圈會比用迭代器迴圈(這裡的迭代器也可以用for-each替換,上面說了它們底層整體是一樣的)要快。至於這一點是為什麼,可以看下ArrayList的原始碼。它的迭代器雖然也是通過下標直接訪問elementData陣列,但是迭代器多了很多方法呼叫以及其他的額外操作,現在很多編譯器cpu也都會對傳統for迴圈進行特別的優化,在這個層面十幾個指令的差別就很大了,這些因素加在一起導致RandomAccess的迭代器比傳統for迴圈要慢一些。對於ArrayList這種,在cpu密集型的應用中應該只使用傳統for迴圈,在迴圈體執行時間比較長的應用中,傳統for迴圈和迭代器迴圈的差別就很小了,這時候使用迭代器(for-each迴圈)也不會明顯降低執行效率。
參考: 1、https://docs.oracle.com/javase/8/docs/technotes/guides/language/foreach.html 2、Java虛擬機器規範(Java SE 8)
以上內容如有問題,煩請指出,謝謝!
package iter;
public class TestArray {
public static void main(String[] args) {
//String[] a = {"a", "b", "c"};
long[] a = {2L, 3L, 5L};
for (long i : a) {
System.err.println(i);
}
}
}
TestArray使用for-each,TestArrayFor使用傳統for迴圈,使用long陣列是為了位元組碼中好區分int/long。 用javap -c看下兩個類的位元組碼操作,儲存成了文字,具體情況如下。package iter; public class TestArrayFor { public static void main(String[] args) { //String[] a = {"a", "b", "c"}; long[] a = {2L, 3L, 5L}; for (int i = 0, len = a.length; i < len; i++) { System.err.println(a[i]); } } }
Compiled from "TestArray.java" public class iter.TestArray { public iter.TestArray(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: newarray long 3: dup 4: iconst_0 5: ldc2_w #16 // long 2l 8: lastore 9: dup 10: iconst_1 11: ldc2_w #18 // long 3l 14: lastore 15: dup 16: iconst_2 17: ldc2_w #20 // long 5l 20: lastore 21: astore_1 /* 0-21行,建立long陣列a,並儲存線上程的當前棧幀的區域性變量表的第1格*/ 22: aload_1 /* 讀取儲存線上程的當前棧幀的區域性變量表的第1格的物件的引用,就是讀取陣列a */ 23: dup /* 把a的引用複製一遍並且再放到棧頂 */ 24: astore 6 /* 把棧頂的數存線上程的當前棧幀的區域性變量表的第6格,就是生成a的一個值複製品b並存儲起來,暫時不知道為什麼這裡要複製一次,後面的陣列都還是用a表示 */ 26: arraylength /* 獲取陣列長度a.length */ 27: istore 5 /* 把陣列長度儲存線上程的當前棧幀的區域性變量表的第5格,22-27隱式執行了int len = a.length */ 29: iconst_0 /* 讀取數字0(這個就是普通的0)到棧中 */ 30: istore 4 /* 把數字0放線上程的當前棧幀的區域性變量表的第4格,29-30隱式執行了int i = 0 */ 32: goto 51 /* 無條件跳轉到51那個地方,開始迴圈的程式碼 */ 35: aload 6 /* 讀取陣列a */ 37: iload 4 /* 讀取i */ 39: laload /* 讀取a[i] */ 40: lstore_2 /* 把a[i]存線上程的當前棧幀的區域性變量表的第2格 */ 41: getstatic #22 // Field java/lang/System.err:Ljava/io/PrintStream; /* 獲取類的static屬性,就是System.err */ 44: lload_2 /* 讀取存線上程的當前棧幀的區域性變量表的第2格的資料,就是讀取a[i] */ 45: invokevirtual #28 // Method java/io/PrintStream.println:(J)V /* 執行虛擬機器方法,30-36就是執行System.err.println(a[i]) */ 48: iinc 4, 1 /* 將第4格的數字加1,就是執行i++ */ 51: iload 4 /* 讀取i */ 53: iload 5 /* 讀取a.length */ 55: if_icmplt 35 /* 如果i < len,跳到標記35的那個地方,不滿足就往下 */ 58: return }
本人對照下位元組碼指令表,簡單翻譯了以下,都寫在上面,還算是比較清楚。/**/中的就是本人的註釋,//開頭的是位元組碼自帶的資訊,這些資訊不能完全算是註釋吧,可以算是對位元組碼中出現的常量的一種直白翻譯,讓你看得懂這些常量代表什麼。 通過編譯後的位元組碼可以看出,陣列的for-each和普通的for迴圈底層原理是一樣的,都是用的普通for迴圈的那一套。陣列的for-each比普通for迴圈多一點點操作,理論上是要慢一點點,這個暫時也不知道是為什麼。這也是語法糖的一些代價,語法越簡單,反而越不好進行底層優化。不過這個慢一點那真是一點,在迴圈體比較複雜時,這個差距就更小了,所以基本上可以認為這兩種方式效率一樣。實際中根據自己的情況選擇,如果需要顯式使用下標,就用傳統for迴圈,其他的都可以使用for-each迴圈。Compiled from "TestArrayFor.java" public class iter.TestArrayFor { public iter.TestArrayFor(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: newarray long 3: dup 4: iconst_0 5: ldc2_w #16 // long 2l 8: lastore 9: dup 10: iconst_1 11: ldc2_w #18 // long 3l 14: lastore 15: dup 16: iconst_2 17: ldc2_w #20 // long 5l 20: lastore 21: astore_1 /* 0-21行,建立long陣列a,並儲存線上程的當前棧幀的區域性變量表的第1格*/ 22: iconst_0 /* 讀取數字0(這個就是普通的0)到棧中 */ 23: istore_2 /* 將棧頂的數字0儲存在第二個,22-23就是執行int i = 0; */ 24: aload_1 /* 讀取儲存線上程的當前棧幀的區域性變量表的第1格的物件的引用,就是讀取陣列a */ 25: arraylength /* 獲取陣列長度a.length */ 26: istore_3 /* 把陣列長度儲存線上程的當前棧幀的區域性變量表的第3格,24-26就是執行int len = a.length */ 27: goto 42 /* 無條件跳到標記42的那個地方,開始迴圈的程式碼 */ 30: getstatic #22 // Field java/lang/System.err:Ljava/io/PrintStream; /* 獲取類的static屬性,就是System.err */ 33: aload_1 /* 讀取陣列a */ 34: iload_2 /* 讀取i */ 35: laload /* 讀取a[i] */ 36: invokevirtual #28 // Method java/io/PrintStream.println:(J)V /* 執行虛擬機器方法,30-36就是執行System.err.println(a[i]) */ 39: iinc 2, 1 /* 將第2格的數字加1,就是執行i++ */ 42: iload_2 /* 讀取i */ 43: iload_3 /* 讀取len */ 44: if_icmplt 30 /* 如果i < len,跳到標記30的那個地方,不滿足就往下 */ 47: return }
二、Collection的for-each 還是先貼兩段簡單的對比的程式碼,程式碼邏輯一樣。Java環境使用的是jdk1.8_111。
package iter;
import java.util.ArrayList;
import java.util.List;
public class TestFor {
public static void main(String[] args) {
List<String> listA = new ArrayList<String>();
for(String str : listA) {
System.err.println(str);
}
}
}
package iter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestIter {
public static void main(String[] args) {
List<String> listA = new ArrayList<String>();
for (Iterator<String> iter = listA.iterator(); iter.hasNext();) {
String s = iter.next();
System.err.println(s);
}
}
}
TestFor是for-each迴圈,TestIter是使用迭代器迴圈。
還是跟陣列的一樣分析,貼下編譯後的位元組碼。
Compiled from "TestFor.java"
public class iter.TestFor {
public iter.TestFor();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #19, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_3
15: goto 35
18: aload_3
19: invokeinterface #25, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24: checkcast #31 // class java/lang/String
27: astore_2
28: getstatic #33 // Field java/lang/System.err:Ljava/io/PrintStream;
31: aload_2
32: invokevirtual #39 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: aload_3
36: invokeinterface #45, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
41: ifne 18
44: return
}
Compiled from "TestIter.java"
public class iter.TestIter {
public iter.TestIter();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #19, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_2
15: goto 35
18: aload_2
19: invokeinterface #25, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24: checkcast #31 // class java/lang/String
27: astore_3
28: getstatic #33 // Field java/lang/System.err:Ljava/io/PrintStream;
31: aload_3
32: invokevirtual #39 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: aload_2
36: invokeinterface #45, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
41: ifne 18
44: return
}
這兩段位元組碼中自帶的註釋很多,基本上看得懂,就不添加註釋了。 兩段位元組碼除了幾個變數儲存線上程的當前棧幀的區域性變量表的索引(astore_n,這個n就是索引)不一樣外,其餘的都是一模一樣的。不排除某次編譯後連那幾個索引值也一樣,那就真一模一樣了。位元組碼自帶的註釋都說了,Collection的for-each底層也是使用迭代器來實現的,兩種方式可以說是完全等價的。 對於實現了RandomAccess介面的實現類,因為它們的隨機訪問操作的時間複雜度為O(1),大多數情況使用傳統for迴圈會比用迭代器迴圈(這裡的迭代器也可以用for-each替換,上面說了它們底層整體是一樣的)要快。至於這一點是為什麼,可以看下ArrayList的原始碼。它的迭代器雖然也是通過下標直接訪問elementData陣列,但是迭代器多了很多方法呼叫以及其他的額外操作,現在很多編譯器cpu也都會對傳統for迴圈進行特別的優化,在這個層面十幾個指令的差別就很大了,這些因素加在一起導致RandomAccess的迭代器比傳統for迴圈要慢一些。對於ArrayList這種,在cpu密集型的應用中應該只使用傳統for迴圈,在迴圈體執行時間比較長的應用中,傳統for迴圈和迭代器迴圈的差別就很小了,這時候使用迭代器(for-each迴圈)也不會明顯降低執行效率。
參考: 1、https://docs.oracle.com/javase/8/docs/technotes/guides/language/foreach.html 2、Java虛擬機器規範(Java SE 8)
以上內容如有問題,煩請指出,謝謝!