foreach用错了?
错误认识
学了那么久的Java了,我们大部分人对foreach的认识就是:只要遍历就用foreach语法,但事实上真的是这样么?
for与foreach简单对比
for:可以知道元素的位标、可以通过位标修改元素内容。
foreach:不能修改元素内容、“只读”操作、实质上使用Iterator遍历、但不能像Iterator一样直接用next()或hasNext()控制。
深入foreach
我们可以通过反编译Class文件,查看字节码,了解底层的实现方式。
反编译方式:
javap -verbose MainClass > t.txt
在命令行执行该命令,在相同路径下生成t.txt文件,文件中存放的就是字节码。
部分JVM字节码解析:
字节码 | 解释 |
---|---|
aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
aload_0 | 将第一个局部变量的引用值压进栈 |
anewarray | 栈顶数值作为数组长度,创建引用型数组,栈顶数值出栈,数组引用进栈 |
arraylength | 栈顶数组引用出栈,数组长度进栈 |
astore_1 | 将栈顶引用数值存入当前frame(域)第二个局部变量中 |
dup | 复制栈顶数值 |
iconst | 将int型常量值压进栈 |
iconst_0 | 将int类型0值压进栈 |
ldc | 将int、float、String常量值从常量池推至(push)栈顶 |
示例
对数组使用foreach语法
java代码
public class MainClass {
public static final void main(String[] args) {
String[] stringArray = new String[]{"test > 1", "test > 2"};
for (String str : stringArray) {
System.out.println(str);
}
}
}
反编译出的字节码
Classfile /C:/Users/Administrator/Desktop/MainClass.class
Last modified 2016-12-8; size 579 bytes
MD5 checksum 716231b8e350a3d39604b15922cfb8c1
Compiled from "MainClass.java"
public class MainClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/lang/String
#3 = String #21 // test > 1
#4 = String #22 // test > 2
#5 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #27 // MainClass
#8 = Class #28 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 StackMapTable
#16 = Class #29 // "[Ljava/lang/String;"
#17 = Utf8 SourceFile
#18 = Utf8 MainClass.java
#19 = NameAndType #9:#10 // "<init>":()V
#20 = Utf8 java/lang/String
#21 = Utf8 test > 1
#22 = Utf8 test > 2
#23 = Class #30 // java/lang/System
#24 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#25 = Class #33 // java/io/PrintStream
#26 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#27 = Utf8 MainClass
#28 = Utf8 java/lang/Object
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
{
public MainClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static final void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=4, locals=6, args_size=1
0: iconst_2
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String test > 1
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String test > 2
13: aastore
14: astore_1
15: aload_1
16: astore_2
17: aload_2
18: arraylength
19: istore_3
20: iconst_0
21: istore 4
23: iload 4
25: iload_3
26: if_icmpge 49
29: aload_2
30: iload 4
32: aaload
33: astore 5
35: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
38: aload 5
40: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: iinc 4, 1
46: goto 23
49: return
LineNumberTable:
line 3: 0
line 5: 15
line 6: 35
line 5: 43
line 8: 49
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 23
locals = [ class "[Ljava/lang/String;", class "[Ljava/lang/String;", class "[Ljava/lang/String;", int, int ]
stack = []
frame_type = 248 /* chop */
offset_delta = 25
}
SourceFile: "MainClass.java"
解析:我们只用看main函数里面的东西就好了,对照前面的字节码解析可以大概知道遍历数组,只是简单的进栈出栈。
等同代码:
for (int len = stringArray.length, i = 0; i < len; ++i) {
String str = stringArray[i];
System.out.println(str);
}
对List使用foreach语法
java代码
public class MainClass {
public static final void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("test > 1");
stringList.add("test > 2");
for (String str : stringList) {
System.out.println(str);
}
}
}
字节码
Classfile /C:/Users/Administrator/Desktop/MainClass.class
Last modified 2016-12-8; size 798 bytes
MD5 checksum 9e8aab2da24e08f2a9a5b4de4ced5e3d
Compiled from "MainClass.java"
public class MainClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/util/ArrayList
#3 = Methodref #2.#26 // java/util/ArrayList."<init>":()V
#4 = String #28 // test > 1
#5 = InterfaceMethodref #29.#30 // java/util/List.add:(Ljava/lang/Object;)Z
#6 = String #31 // test > 2
#7 = InterfaceMethodref #29.#32 // java/util/List.iterator:()Ljava/util/Iterator;
#8 = InterfaceMethodref #33.#34 // java/util/Iterator.hasNext:()Z
#9 = InterfaceMethodref #33.#35 // java/util/Iterator.next:()Ljava/lang/Object;
#10 = Class #36 // java/lang/String
#11 = Fieldref #37.#38 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Methodref #39.#40 // java/io/PrintStream.println:(Ljava/lang/String;)V
#13 = Class #41 // MainClass
#14 = Class #42 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 StackMapTable
#22 = Class #43 // java/util/List
#23 = Class #44 // java/util/Iterator
#24 = Utf8 SourceFile
#25 = Utf8 MainClass.java
#26 = NameAndType #15:#16 // "<init>":()V
#27 = Utf8 java/util/ArrayList
#28 = Utf8 test > 1
#29 = Class #43 // java/util/List
#30 = NameAndType #45:#46 // add:(Ljava/lang/Object;)Z
#31 = Utf8 test > 2
#32 = NameAndType #47:#48 // iterator:()Ljava/util/Iterator;
#33 = Class #44 // java/util/Iterator
#34 = NameAndType #49:#50 // hasNext:()Z
#35 = NameAndType #51:#52 // next:()Ljava/lang/Object;
#36 = Utf8 java/lang/String
#37 = Class #53 // java/lang/System
#38 = NameAndType #54:#55 // out:Ljava/io/PrintStream;
#39 = Class #56 // java/io/PrintStream
#40 = NameAndType #57:#58 // println:(Ljava/lang/String;)V
#41 = Utf8 MainClass
#42 = Utf8 java/lang/Object
#43 = Utf8 java/util/List
#44 = Utf8 java/util/Iterator
#45 = Utf8 add
#46 = Utf8 (Ljava/lang/Object;)Z
#47 = Utf8 iterator
#48 = Utf8 ()Ljava/util/Iterator;
#49 = Utf8 hasNext
#50 = Utf8 ()Z
#51 = Utf8 next
#52 = Utf8 ()Ljava/lang/Object;
#53 = Utf8 java/lang/System
#54 = Utf8 out
#55 = Utf8 Ljava/io/PrintStream;
#56 = Utf8 java/io/PrintStream
#57 = Utf8 println
#58 = Utf8 (Ljava/lang/String;)V
{
public MainClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public static final void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String test > 1
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #6 // String test > 2
20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq 62
42: aload_2
43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
48: checkcast #10 // class java/lang/String
51: astore_3
52: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
55: aload_3
56: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: goto 33
62: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 17
line 9: 26
line 10: 52
line 11: 59
line 12: 62
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 33
locals = [ class java/util/List, class java/util/Iterator ]
frame_type = 250 /* chop */
offset_delta = 28
}
SourceFile: "MainClass.java"
解析:可以发现注释中有Iterator。所以对ArrayList作遍历操作,实质上是使用Iterator来遍历。
等同代码:
for (Iterator<String> i = stringList.iterator(); i.hasNext();) {
String str = i.next();
System.out.println(str);
}
小结
对数组使用foreach语法,实质上转化成普通的for循环实现。而对List使用foreach语法,实质上为Iterator实现,与普通for循环实现完全不同。
再深入
而到这里,查看ArrayList源码,会发现ArrayList实现了RandomAccess接口,我们再查看一下RandomAccess源码,会发现以下注释。
Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good performance when applied to either random or sequential access lists.
List的实现使用的标记接口,指明了它们支持快速随机访问。该接口的最初目的是:当运用到随机访问和连续访问List时,为了提高性能,允许一般的算法改变其行为。
The best algorithms for manipulating random access lists (such as ArrayList) can produce quadratic behavior when applied to sequential access lists (such as LinkedList). Generic list algorithms are encouraged to check whether the given list is an instanceof this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable performance.
一个在随机访问List的最佳算法运用到连续访问List中会产生二次项行为(???)推荐一般的List算法在运用前根据判断List是否instanceof该接口,来做不同的处理,以保证良好的性能。
It is recognized that the distinction between random and sequential access is often fuzzy. For example, some List implementations provide asymptotically linear access times if they get huge, but constant access times in practice. Such a List implementation should generally implement this interface. As a rule of thumb, a List implementation should implement this interface if, for typical instances of the class, this loop:
for (int i=0, n=list.size(); i < n; i++) list.get(i);runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); ) i.next();现在认识到随机访问和连续访问的界限是模糊的。例如,一些List的实现如果变的非常大的时候就会表现出渐进线性访问时间,而实施中却是恒定不变的。这样的List实现类应实现该接口。使用for遍历实现改接口的List的效率比foreach快很多。
所以,实现了RandomAccess的List(例如ArrayList),在遍历的时候使用for循环而不使用foreach。而对于没有实现RandomAccess的List(例如LinkedList),遍历使用Iterator更有效率一点。