哪个更有效,for-each 循环还是迭代器?
Posted
技术标签:
【中文标题】哪个更有效,for-each 循环还是迭代器?【英文标题】:Which is more efficient, a for-each loop, or an iterator? 【发布时间】:2011-01-07 23:10:09 【问题描述】:遍历集合的最有效方法是什么?
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 的建议,我将发布我对这个问题的答案。
【问题讨论】:
我认为它并没有什么不同,因为它的 Java 和模板机制只不过是语法糖 潜在重复:***.com/questions/89891/… @OMG Ponies:我不相信这是重复的,因为它不会将循环与迭代器进行比较,而是询问为什么集合返回迭代器,而不是直接使用迭代器在课堂上。 【参考方案1】:foreach
underhood 正在创建iterator
,调用 hasNext() 并调用 next() 获取值;仅当您使用实现 RandomomAccess 的东西时,才会出现性能问题。
for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext())
CustomObj custObj = iter.next();
....
基于迭代器的循环的性能问题是因为它是:
-
即使列表为空也分配对象 (
Iterator<CustomObj> iter = customList.iterator();
);
iter.hasNext()
在循环的每次迭代中都有一个 invokeInterface 虚拟调用(遍历所有类,然后在跳转前进行方法表查找)。
迭代器的实现必须至少进行 2 个字段查找,以使 hasNext()
调用图的值:#1 获取当前计数,#2 获取总计数
在主体循环内部,还有另一个invokeInterface 虚拟调用iter.next
(所以:在跳转之前遍历所有类并进行方法表查找)并且还必须进行字段查找:#1 获取索引和# 2 获取对数组的引用以对其进行偏移(在每次迭代中)。
一个潜在的优化是使用缓存大小查找切换到index iteration
:
for(int x = 0, size = customList.size(); x < size; x++)
CustomObj custObj = customList.get(x);
...
我们有:
-
一个invokeInterface虚拟方法调用
customList.size()
在for循环初始创建时获取大小
在for循环体中调用get方法customList.get(x)
,这是对数组的字段查找,然后可以对数组进行偏移
我们减少了大量的方法调用和字段查找。这是你不想用LinkedList
或不是RandomAccess
集合obj 的东西,否则customList.get(x)
会变成每次迭代都必须遍历LinkedList
的东西。
当您知道这是任何基于 RandomAccess
的列表集合时,这是完美的。
【讨论】:
【参考方案2】: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()
并继续循环
至于“为什么这个无用的循环没有从编译后的代码中得到优化?我们可以看到它对列表项没有任何作用”:嗯,你可以编写你的迭代代码这样@ 987654333@ 有副作用,或者说.hasNext()
有副作用或有意义的后果。
您可以很容易地想象,表示来自数据库的可滚动查询的迭代可能会在 .hasNext()
上做一些戏剧性的事情(比如联系数据库,或者因为您已经到达结果集的末尾而关闭游标)。
因此,即使我们可以证明循环体中没有发生任何事情……但证明在我们迭代时没有发生任何有意义/后果性的事情会更加昂贵(难以处理?)。编译器必须将这个空循环体留在程序中。
我们所能期望的最好的结果就是编译器警告。有趣的是,javac -Xlint:all Whatever.java
确实没有警告我们这个空循环体。 IntelliJ IDEA 可以。诚然,我已将 IntelliJ 配置为使用 Eclipse 编译器,但这可能不是原因。
【讨论】:
【参考方案3】:Iterator 是 Java Collections 框架中的一个接口,它提供了遍历或迭代集合的方法。
当您的动机只是遍历集合以读取其元素时,迭代器和 for 循环的行为相似。
for-each
只是迭代集合的一种方式。
例如:
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 循环和迭代器的例子。
当您尝试修改集合时,差异就出现了。在这种情况下,迭代器的效率更高,因为它具有快速故障属性。 IE。它在遍历下一个元素之前检查基础集合结构中的任何修改。如果发现任何修改,它将抛出 ConcurrentModificationException。
(注意:迭代器的此功能仅适用于 java.util 包中的集合类。它不适用于并发集合,因为它们本质上是故障安全的)
【讨论】:
您关于差异的说法不正确,for each 循环在水下也使用迭代器,因此具有相同的行为。 @Pault Wagland,我已经修改了我的答案,谢谢你指出错误 您的更新仍然不准确。您拥有的两个代码 sn-ps 由语言定义为相同。如果行为有任何差异,那就是实现中的错误。唯一的区别是您是否有权访问迭代器。 @Paul Wagland 即使您使用 for 每个使用迭代器的循环的默认实现,如果您尝试在并发操作期间使用 remove() 方法,它仍然会抛出异常。查看以下内容以获取更多信息here 使用 for each 循环,您无法访问迭代器,因此您无法对其调用 remove。但这不是重点,在您的回答中,您声称一个是线程安全的,而另一个不是。根据语言规范,它们是等价的,因此它们都只与底层集合一样线程安全。【参考方案4】:如果您只是在集合中徘徊以读取所有值,那么使用迭代器或新的 for 循环语法没有区别,因为新语法只是在水下使用迭代器。
但是,如果您指的是循环旧的“c 样式”循环:
for(int i=0; i<list.size(); i++)
Object o = list.get(i);
然后新的 for 循环或迭代器可能会更有效,具体取决于底层数据结构。原因是对于某些数据结构,get(i)
是 O(n) 操作,这使得循环成为 O(n2) 操作。传统的链表就是这种数据结构的一个例子。所有迭代器都有一个基本要求,即next()
应该是一个 O(1) 操作,使得循环 O(n)。
要验证新的 for 循环语法是否在水下使用了迭代器,请比较以下两个 Java sn-ps 生成的字节码。首先是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 循环,因为它的样板代码较少。
【讨论】:
我相信他说的恰恰相反,foo.get(i) 的效率可能会低很多。想想LinkedList。如果你在 LinkedList 的中间执行 foo.get(i),它必须遍历所有先前的节点才能到达 i。另一方面,迭代器将保留底层数据结构的句柄,并允许您一次遍历一个节点。 不是什么大事,但for(int i; i < list.size(); i++)
样式循环也必须在每次迭代结束时评估list.size()
,如果使用它,有时首先缓存list.size()
的结果会更有效。
实际上,对于 ArrayList 和所有其他实现 RandomAccess 接口的情况,原始陈述也是如此。 “C 风格”循环比基于迭代器的循环更快。 docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
无论是 foreach 还是 desugar'd 版本,使用旧的 C 样式循环而不是迭代器方法的一个原因是垃圾。许多数据结构在调用 .iterator() 时会实例化一个新的迭代器,但是可以使用 C 风格的循环无分配地访问它们。这在某些高性能环境中可能很重要,在这些环境中,人们试图避免 (a) 命中分配器或 (b) 垃圾回收。
正如另一条评论,对于 ArrayLists,for(int i = 0 .... ) 循环比使用迭代器或 for (:) 方法快大约 2 倍,因此它确实取决于关于底层结构。顺便说一句,迭代 HashSets 也非常昂贵(比数组列表要多得多),所以要避免像瘟疫一样的东西(如果可以的话)。【参考方案5】:
在使用集合时,我们应该避免使用传统的 for 循环。 我将给出的简单原因是 for 循环的复杂度为 O(sqr(n)) 量级,而 Iterator 甚至增强的 for 循环的复杂度仅为 O(n)。 所以它给出了性能差异.. 只需列出大约 1000 个项目并使用两种方式打印即可。并打印执行的时间差。您可以看到差异。
【讨论】:
请添加一些说明性示例来支持您的陈述。 @Chandan 抱歉,您写的内容有误。例如:std::vector 也是一个集合,但它的访问成本为 O(1)。所以传统的对向量的 for 循环只是 O(n)。我想你想说,如果底层容器的访问具有 O(n) 的访问成本,那么对于 std::list 来说,它的复杂性是 O(n^2)。在这种情况下使用迭代器会将成本降低到 O(n),因为迭代器允许直接访问元素。 如果您进行时间差计算,请确保两个集合都已排序(或公平分布的随机未排序)并为每个集合运行两次测试并仅计算每个集合的第二次运行。用这个再次检查你的时间(它是关于为什么你需要运行两次测试的一个很长的解释)。您需要证明(可能使用代码)这是如何正确的。否则,据我所知,两者在性能方面是相同的,但不是能力。【参考方案6】:为了扩展 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()
使局部变量可用
因此,换句话说,对于所有实际目的,字节码将是相同的,或几乎相同的。很难想象任何编译器实现会导致两者之间有任何显着差异。
【讨论】:
实际上,我所做的测试是使用 Eclipse 编译器进行的,但您的一般观点仍然成立。 +1【参考方案7】:区别不在于性能,而在于能力。当直接使用引用时,您对显式使用一种迭代器有更大的权力(例如 List.iterator() 与 List.listIterator(),尽管在大多数情况下它们返回相同的实现)。您还可以在循环中引用迭代器。这使您可以执行诸如从集合中删除项目之类的操作,而不会收到 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);
【讨论】:
这是非常正确的,即使它没有直接回答我给它 +1 的问题,因为它提供了信息,并回答了合乎逻辑的后续问题。 是的,我们可以使用 foreach 循环访问集合元素,但我们不能删除它们,但我们可以使用 Iterator 删除元素。以上是关于哪个更有效,for-each 循环还是迭代器?的主要内容,如果未能解决你的问题,请参考以下文章
有没有办法在 for-each 循环迭代开始之前避免空检查? [复制]
集合foreach迭代时,边迭代边删除,只能使用迭代器删除,不能使用集合删除,否则会导致并发修改异常for-each和Iterator的选择
如何在没有 ConcurrentModificationException 的情况下使用 for-each 循环进行迭代时修改集合? [复制]