哪个更有效,一个for-each循环或一个迭代器?


206

遍历集合的最有效方法是哪种?

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();
}

请注意,尽管最后一个问题的答案之一接近,但这不是thisthisthisthis的精确重复。之所以不是这样,是因为其中大多数是比较循环get(i),而不是使用迭代器,而您在循环中调用该循环。

正如建议,我将发布我对这个问题的答案。


我认为它没有什么用,因为它的Java和模板机制不过是语法糖而已
Hassan Syed 2010年


2
@OMG Ponies:我不认为这是重复的,因为这没有将循环与迭代器进行比较,而是询问为什么集合返回迭代器,而不是直接将迭代器直接放在类上。
Paul Wagland 2010年

Answers:


264

如果您只是在集合上徘徊以读取所有值,那么使用迭代器或新的for循环语法之间就没有区别,因为新语法仅在水下使用迭代器。

但是,如果您是指循环旧的“ c-style”循环:

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循环,因为它具有较少的样板代码,因此您应该选择最美观的循环形式。


4
我相信他在说相反的话,foo.get(i)的效率可能低很多。想想LinkedList。如果在LinkedList的中间执行foo.get(i),则必须遍历所有先前的节点才能到达i。另一方面,迭代器将保留底层数据结构的句柄,并允许您一次遍历节点。
Michael Krauklis,2010年

1
这不是什么大不了的事,但是for(int i; i < list.size(); i++) {样式循环也必须list.size()在每次迭代结束时求值,如果使用它,则缓存list.size()first 的结果有时会更有效。
Brett Ryan

3
实际上,对于ArrayList和所有其他实现RandomAccess接口的情况,原始语句也适用。“ C样式”循环比基于Iterator的循环更快。 docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp 2013年

4
使用旧的C样式循环而不是Iterator方法的原因之一就是垃圾,无论它是foreach版本还是desugar版本。当调用.iterator()时,许多数据结构实例化一个新的Iterator,但是可以使用C样式循环以无分配方式访问它们。这在某些高性能环境中很重要,在这种环境中,人们试图避免(a)触及分配器或(b)垃圾回收。
2013年

3
就像另一条评论一样,对于ArrayLists,for(int i = 0 ....)循环比使用迭代器或for(:)方法快约2倍,因此它确实取决于底层结构。另外,迭代HashSet也非常昂贵(比数组列表要多得多),因此请避免使用类似瘟疫的代码(如果可以的话)。
2014年

106

差异不在于性能,而在于功能。直接使用引用时,可以显式地使用某种迭代器(例如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);
     }
}

12
这是非常正确的,即使它没有直接回答我已经给出的+1信息,也不能回答逻辑上的后续问题。
保罗·瓦格兰

1
是的,我们可以使用foreach循环访问集合元素,但是不能删除它们,但是可以使用Iterator删除元素。
Akash5288

22

为了扩展Paul自己的答案,他证明了特定编译器(大概是Sun的javac?)上的字节码是相同的,但是不能保证不同的编译器会生成相同的字节码,对吗?要了解两者之间的实际区别,让我们直接看一下源代码并检查Java语言规范,特别是14.14.2,“ for语句的增强”

增强的for语句等效于for以下形式的基本语句:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

换句话说,JLS要求两者相等。从理论上讲,这可能意味着字节码中的边际差异,但实际上,需要增强的for循环来:

  • 调用.iterator()方法
  • .hasNext()
  • 通过以下方式使局部变量可用 .next()

因此,换句话说,出于所有实际目的,字节码将是相同或几乎相同的。很难设想任何编译器实现都会导致两者之间的重大差异。


实际上,我所做的测试是使用Eclipse编译器进行的,但您的一般观点仍然成立。+1
保罗·瓦格兰

3

foreach发动机罩被创建iterator,调用hasNext()和调用next()来获取价值; 仅当您使用实现RandomomAccess的东西时,才会出现性能问题。

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

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

  1. 分配一个对象,即使列表为空(Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() 在循环的每次迭代期间,都有一个invokeInterface虚拟调用(遍历所有类,然后在跳转之前进行方法表查找)。
  3. 迭代器的实现必须至少进行2个字段查找才能使hasNext()调用值成为值:#1获取当前计数,#2获取总计数
  4. 在主体循环中,还有另一个invokeInterface虚拟调用iter.next(因此:在跳转之前遍历所有类并进行方法表查找),还必须进行字段查找:#1获取索引,#2获取对引用的引用数组来执行偏移量(在每次迭代中)。

可能的优化方法是index iteration使用缓存的大小查找切换到

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

这里我们有:

  1. customList.size()在for循环的初始创建中调用一个invokeInterface虚拟方法以获取大小
  2. customList.get(x)在body循环期间进行get方法调用,这是对数组的字段查找,然后可以对数组进行偏移

我们减少了大量的方法调用和字段查找。这是您不希望使用的对象LinkedList或不是RandomAccess集合obj的对象,否则,customList.get(x)它将变成LinkedList每次迭代都必须遍历的对象。

当您知道这是任何RandomAccess基于列表的集合时,这是完美的。


1

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()并继续循环

至于“为什么这个无用的循环为什么没有从编译后的代码中得到优化呢?我们可以看到它对列表项没有任何作用”:嗯,您有可能对可迭代的代码进行编码,从而.iterator()产生副作用,否则.hasNext()会有副作用或有意义的后果。

您很容易想到,一个表示数据库中可滚动查询的可迭代对象可能会产生重大变化.hasNext()(例如,与数据库联系,或者因为到达结果集的末尾而关闭游标)。

因此,即使我们可以证明循环主体中什么也没有发生……要证明迭代时什么也没有有意义/结果没有发生,这是更昂贵的(难处理的吗?)。编译器必须将此空循环体留在程序中。

我们所希望的最好的结果是编译器警告。有趣的是javac -Xlint:all Whatever.java没有警告我们有关此空循环的主体。IntelliJ IDEA可以。诚然,我已经将IntelliJ配置为使用Eclipse Compiler,但这可能不是原因。

在此处输入图片说明


0

迭代器是Java Collections框架中的一个接口,它提供遍历或迭代集合的方法。

当您的动机只是遍历集合以读取其元素时,迭代器和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循环和迭代器的情况。

当您尝试修改集合时会有所不同。在这种情况下,迭代器由于其fail-fast属性而更加高效。即。它在遍历下一个元素之前检查基础集合的结构是否有任何修改。如果找到任何修改,它将抛出ConcurrentModificationException

(注意:迭代器的此功能仅适用于java.util包中的集合类。不适用于并发集合,因为它们本质上是故障安全的)


1
您关于差异的陈述是不正确的,for每个循环还在水下使用迭代器,因此具有相同的行为。
Paul Wagland

@Pault Wagland,我修改了我的答案,谢谢您指出错误
eccentricCoder

您的更新仍然不准确。语言定义的两个代码段相同。如果行为上有任何差异,则说明实施中存在错误。唯一的区别是您是否有权访问迭代器。
Paul Wagland

@Paul Wagland即使您为使用迭代器的每个循环使用默认实现,如果您在并发操作期间尝试使用remove()方法,它仍然会抛出异常。在此处
eccentricCoder

1
使用for each循环,您将无法访问迭代器,因此无法对其调用remove。但这不是重点,在您的回答中您声称一个是线程安全的,而另一个则不是。根据语言规范,它们是等效的,因此它们两者的安全性仅与基础集合相同。
Paul Wagland

-8

在使用集合时,我们应避免使用传统的for循环。我给出的简单原因是for循环的复杂度约为O(sqr(n)),而Iterator甚至增强的for循环的复杂度仅为O(n)。因此,它会产生性能差异。.只需列出约1000个项目的清单,然后使用两种方法进行打印即可。并打印执行时差。您会看到差异。


请添加一些说明性示例以支持您的陈述。
Rajesh Pitty

@Chandan对不起,但您写的是错误的。例如:std :: vector也是一个集合,但其访问成本为O(1)。因此,向量的传统for循环仅为O(n)。我想您想说,如果对底层容器的访问具有O(n)的访问成本,那么对于std :: list而言,则比O(n ^ 2)复杂。在这种情况下,使用迭代器将降低O(n)的成本,因为迭代器允许直接访问元素。
kaiser 2015年

如果您进行时间差计算,请确保两个集合都已排序(或相当随机分布的随机未排序),并且对每个集合运行两次测试,并仅计算每个集合的第二次运行。以此再次检查您的时间(这是为什么需要两次运行测试的长篇解释)。您需要证明(也许用代码)这是真的。否则,据我所知,两者在性能方面是相同的,但在功能方面却是相同的。
ydobonebi '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.