哪种方式最有效的遍历集合?
List<Integer> a = new ArrayList<Integer>();
for (Integer integer : a) {
integer.toString();
}
或
List<Integer> a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
Integer integer = (Integer) iterator.next();
integer.toString();
}
请注意,这与this,this,this或this不完全相同,尽管上一个问题的答案之一已接近。这不是欺骗的原因是,大多数都是在循环中调用get(i)
的循环,而不是使用迭代器。
根据Meta的建议,我将发布我对此问题的回答。
答案 0 :(得分:253)
如果你只是漫游集合来读取所有值,那么使用迭代器或新的for循环语法之间没有区别,因为新语法只是在水下使用迭代器。
但是,如果你的意思是循环旧的“c风格”循环:
for(int i=0; i<list.size(); i++) {
Object o = list.get(i);
}
然后,新的for循环或迭代器可以更高效,具体取决于底层数据结构。原因是对于某些数据结构,get(i)
是O(n)操作,这使得循环成为O(n 2 )操作。传统的链表是这种数据结构的一个例子。所有迭代器都有一个基本要求next()
应该是一个O(1)操作,使循环为O(n)。
要通过新的for循环语法验证迭代器是否在水下使用,请比较以下两个Java代码段中生成的字节码。首先是for循环:
List<Integer> a = new ArrayList<Integer>();
for (Integer integer : a)
{
integer.toString();
}
// Byte code
ALOAD 1
INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
ASTORE 3
GOTO L2
L3
ALOAD 3
INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ASTORE 2
ALOAD 2
INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
POP
L2
ALOAD 3
INVOKEINTERFACE java/util/Iterator.hasNext()Z
IFNE L3
第二,迭代器:
List<Integer> a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
Integer integer = (Integer) iterator.next();
integer.toString();
}
// Bytecode:
ALOAD 1
INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
ASTORE 2
GOTO L7
L8
ALOAD 2
INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ASTORE 3
ALOAD 3
INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
POP
L7
ALOAD 2
INVOKEINTERFACE java/util/Iterator.hasNext()Z
IFNE L8
如您所见,生成的字节代码实际上是相同的,因此使用任何一种形式都不会有性能损失。因此,对于大多数将成为for-each循环的人来说,你应该选择最具美学吸引力的循环形式,因为它具有较少的样板代码。
答案 1 :(得分:100)
差异不在于性能,而在于能力。直接使用引用时,您可以使用迭代器类型(例如List.iterator()与List.listIterator()进行显式更强大的功能,尽管在大多数情况下它们会返回相同的实现)。您还可以在循环中引用Iterator。这允许您执行诸如从集合中删除项而不会收到ConcurrentModificationException之类的操作。
没关系:
Set<Object> set = new HashSet<Object>();
// add some items to the set
Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
Object o = setIterator.next();
if(o meets some condition){
setIterator.remove();
}
}
这不是,因为它会引发并发修改异常:
Set<Object> set = new HashSet<Object>();
// add some items to the set
for(Object o : set){
if(o meets some condition){
set.remove(o);
}
}
答案 2 :(得分:22)
为了扩展Paul自己的答案,他已经证明字节码在特定编译器上是相同的(可能是Sun的javac?)但是不同的编译器不能保证生成相同的字节码,对吧?要了解两者之间的实际差异,让我们直接查看源代码并检查Java语言规范,特别是14.14.2, "The enhanced for statement":
增强的
for
语句相当于表单的基本for
语句:
for (I #i = Expression.iterator(); #i.hasNext(); ) {
VariableModifiers(opt) Type Identifier = #i.next();
Statement
}
换句话说,JLS要求两者是等价的。从理论上讲,这可能意味着字节码的边际差异,但实际上需要增强的for循环:
.iterator()
方法.hasNext()
.next()
因此,换句话说,出于所有实际目的,字节码将是相同的或几乎相同的。很难设想任何可能导致两者之间存在显着差异的编译器实现。
答案 3 :(得分:1)
foreach
发动机罩正在创建iterator
,调用hasNext()并调用next()来获取值;只有在使用实现RandomomAccess的东西时才会出现性能问题。
for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
CustomObj custObj = iter.next();
....
}
基于迭代器的循环的性能问题是因为它是:
Iterator<CustomObj> iter = customList.iterator();
); iter.hasNext()
在循环的每次迭代中都有一个invokeInterface虚拟调用(遍历所有类,然后在跳转之前执行方法表查找)。hasNext()
调用数值:#1获取当前计数,#2获取总计数iter.next
(所以:遍历所有类并在跳转之前进行方法表查找)并且还必须进行字段查找:#1获取索引和#2获取对数组的引用以进行偏移(在每次迭代中)。 潜在的优化是使用缓存的大小查找切换到index iteration
:
for(int x = 0, size = customList.size(); x < size; x++){
CustomObj custObj = customList.get(x);
...
}
我们有:
customList.size()
初始创建for循环以获取大小customList.get(x)
,这是对数组的字段查找,然后可以将偏移量放入数组中我们减少了大量的方法调用,字段查找。您不希望使用LinkedList
或不属于RandomAccess
集合obj的内容,否则customList.get(x)
将变成必须遍历LinkedList
的内容1}}每次迭代。
当您知道任何基于RandomAccess
的列表集时,这是完美的。
答案 4 :(得分:0)
Iterator是Java Collections框架中的一个接口,它提供遍历或迭代集合的方法。
当你的动机只是遍历一个集合来读取它的元素时,iterator和for循环的行为都相似。
for-each
只是迭代Collection的一种方法。
例如:
List<String> messages= new ArrayList<>();
//using for-each loop
for(String msg: messages){
System.out.println(msg);
}
//using iterator
Iterator<String> it = messages.iterator();
while(it.hasNext()){
String msg = it.next();
System.out.println(msg);
}
for-each循环只能用于实现迭代器接口的对象。
现在回到for循环和迭代器的情况。
当您尝试修改集合时会出现差异。在这种情况下,迭代器因其 故障快速属性 而更有效。即。它在迭代下一个元素之前检查底层集合结构的任何修改。如果找到任何修改,它将抛出 ConcurrentModificationException 。
(注意:迭代器的这个功能仅适用于java.util包中的集合类。它不适用于并发集合,因为它们本质上是故障安全的)
答案 5 :(得分:0)
foreach
无论如何都使用了引擎盖下的迭代器。它真的只是语法糖。
考虑以下计划:
import java.util.List;
import java.util.ArrayList;
public class Whatever {
private final List<Integer> list = new ArrayList<>();
public void main() {
for(Integer i : list) {
}
}
}
让我们用javac Whatever.java
编译它,
并使用main()
:
javap -c Whatever
的反汇编字节码
public void main();
Code:
0: aload_0
1: getfield #4 // Field list:Ljava/util/List;
4: invokeinterface #5, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_1
10: aload_1
11: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
16: ifeq 32
19: aload_1
20: invokeinterface #7, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
25: checkcast #8 // class java/lang/Integer
28: astore_2
29: goto 10
32: return
我们可以看到foreach
编译成一个程序:
List.iterator()
Iterator.hasNext()
:调用Iterator.next()
并继续循环至于&#34;为什么这个无用的循环不会从编译的代码中优化出来?我们可以看到它没有对列表项做任何事情&#34;:嗯,您可以对您的迭代进行编码,使.iterator()
有副作用,或者{ {1}}会产生副作用或产生有意义的后果。
您可以很容易地想象,表示来自数据库的可滚动查询的迭代可能会在.hasNext()
上做一些戏剧性的事情(比如联系数据库,或关闭游标,因为您已达到结果集的末尾) )。
所以,尽管我们可以证明循环体中没有任何反应......但是,当我们迭代时,证明没有任何有意义/重要的事情会更加昂贵(难以处理?)。编译器必须将这个空循环体留在程序中。
我们希望的最好的是编译器警告。有趣的是.hasNext()
没有警告我们这个空循环体。但IntelliJ IDEA确实如此。不可否认,我已将IntelliJ配置为使用Eclipse编译器,但这可能不是原因。
答案 6 :(得分:-8)
在使用Collections时,我们应该避免使用传统的for循环。 我将给出的简单原因是for循环的复杂性是O(sqr(n))的顺序,Iterator的复杂性甚至增强的for循环只是O(n)。 所以它给出了一个性能差异.. 只需列出大约1000个项目并使用两种方式打印。并打印执行的时差。你可以看到差异。