哪个更有效,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】:

foreachunderhood 正在创建iterator,调用 hasNext() 并调用 next() 获取值;仅当您使用实现 RandomomAccess 的东西时,才会出现性能问题。

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext())
   CustomObj custObj = iter.next();
   ....

基于迭代器的循环的性能问题是因为它是:

    即使列表为空也分配对象 (Iterator&lt;CustomObj&gt; 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 &lt; 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 循环迭代开始之前避免空检查? [复制]

2优先使用for-each循环

集合foreach迭代时,边迭代边删除,只能使用迭代器删除,不能使用集合删除,否则会导致并发修改异常for-each和Iterator的选择

如何在没有 ConcurrentModificationException 的情况下使用 for-each 循环进行迭代时修改集合? [复制]

java中for循环和迭代器哪个效率高?

关于迭代器的for-each遍历集合现象。。。。。