代码之家  ›  专栏  ›  技术社区  ›  Paul Wagland

哪个更有效,一个for each循环,还是一个迭代器?

  •  187
  • Paul Wagland  · 技术社区  · 15 年前

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

    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 我会把我对这个问题的答案贴出来。

    7 回复  |  直到 7 年前
        1
  •  250
  •   Paul Wagland    12 年前

    如果您只是在集合中漫游以读取所有值,那么使用迭代器和新的for循环语法之间没有区别,因为新的语法只是在水下使用迭代器。

    但是,如果您的意思是循环旧的“C样式”循环:

    for(int i=0; i<list.size(); i++) {
       Object o = list.get(i);
    }
    

    然后,根据底层数据结构,新的for循环(或迭代器)可能更高效。这是因为对于某些数据结构, get(i) 是O(N)操作,使循环成为O(N) )手术。传统的链表就是这种数据结构的一个例子。所有迭代器都有一个基本要求, 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
    

    如您所见,所生成的字节代码实际上是相同的,因此使用这两种形式都不会造成性能损失。因此,对于大多数人来说,你应该选择对你最有吸引力的循环形式,因为它的样板代码更少。

        2
  •  99
  •   Michael Krauklis    15 年前

    区别不在于性能,而在于性能。直接使用引用时,使用迭代器类型(例如,list.iterator()与list.listirator()相比,尽管在大多数情况下它们返回相同的实现),您可以更有效地显式地转换。您还可以在循环中引用迭代器。这允许您在不获取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);
         }
    }
    
        3
  •  20
  •   Cowan    15 年前

    为了扩展保罗自己的答案,他已经证明了字节码在特定的编译器上是相同的(大概是Sun的javac?)但不同的编译器 放心 生成相同的字节码,对吗?为了了解两者之间的实际差异,我们直接进入源代码,并具体检查Java语言规范。 14.14.2, "The enhanced for statement" :

    增强型 for 语句等价于 对于 表格说明:

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

    换言之,JLS要求两者等效。理论上,这可能意味着字节码的边际差异,但实际上,增强的for循环需要:

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

    所以,换句话说,对于所有实际目的,字节码都是相同的,或者几乎相同的。很难想象任何编译器实现会导致两者之间的任何显著差异。

        4
  •  1
  •   denis_lor    7 年前

    这个 foreach 引擎盖下的 iterator ,调用hasNext()并调用next()以获取值;只有在使用实现RandomMaccess的东西时,才会出现性能问题。

    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() 调用Figure值:1获取当前计数,2获取总计数
    4. 在body循环中,有另一个invokeInterface虚拟调用 iter.next (所以:在跳转之前遍历所有类并执行方法表查找),并且还必须执行字段查找:1获取索引,2获取对数组的引用,以便(在每次迭代中)执行对数组的偏移。

    一个潜在的优化是切换到 index iteration 使用缓存大小查找:

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

    这里我们有:

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

    我们减少了大量的方法调用和字段查找。你不想做的事 LinkedList 或者有一些不是 RandomAccess 托收对象,否则 自定义列表.get(x) 会变成必须穿过 链表 每次迭代。

    如果你知道这是完美的 随机存取 基于列表集合。

        5
  •  0
  •   eccentricCoder    8 年前

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

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

    当您试图修改一个集合时,会出现不同的情况。在这种情况下,迭代器由于其 fail fast属性 . 也就是说,它在遍历下一个元素之前检查基础集合结构中的任何修改。如果发现任何修改,它将 当前修改例外 .

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

        6
  •  0
  •   Birchlabs    8 年前

    foreach uses iterators under the hood anythy.它真的只是句法上的糖分。

    考虑以下程序:

    import java.util.list;
    导入java.util.arraylist;
    
    公务舱什么的{
    private final list<integer>list=new arraylist<gt;();
    公共void main()。{
    对于(整数i:列表){
    }
    }
    }
    < /代码> 
    
    

    让我们用<代码> javac编译它。Java < /> >,
    并使用javap-c whatever读取已反汇编的字节码

    public void main();
    代码:
    0:ALOADY0
    1:getfield 4//字段列表:ljava/util/list;
    4:调用接口5, 1//接口方法Java/UTL/List.Idter:()Ljava/UTL/Idter;
    9:阿斯特一号
    10:ALOAD1
    11:调用接口6, 1//接口方法Java/UTI/ItActual.HasNeX:()z
    16:IFEQ 32
    19:ALOAD1
    20:调用接口7, 1//接口方法Java/UTI/IATORATION.下一步:()LJava/Lang/Objor;
    25:CQuasCAST 8//类Java/Lang/整数
    28:AttoReS2
    29:转到10
    32:返回
    < /代码> 
    
    

    我们可以看到foreachcompiled down to a program which:。

    • 使用list.iterator()创建迭代器
    • ifiterator.hasNext():invokesiterator.next()and continues loop

    至于“为什么这个无用的循环不能从编译的代码中得到优化?”我们可以看到,它对列表项没有任何作用:“好吧,您可以对您的iterable进行编码,例如,iterator()has side-effects,or so thathasNext()has side-effects or meanisable results.

    可以很容易地想象,表示数据库中可滚动查询的iterable可能会在.hasNext()上执行一些戏剧性的操作(如联系数据库,或关闭光标,因为您已经到达结果集的末尾)。

    所以,即使我们能证明在环体中什么都没有发生,它还是更昂贵(难处理?)证明当我们迭代时没有任何有意义的/结果性的事情发生。编译器必须将这个空循环体留在程序中。

    我们最希望的是编译器。有趣的是,<代码> javac -xLnt:所有的一切。Java < /C> > < E> > 警告我们关于这个空循环体。不过,Intellij的想法确实如此。诚然,我已经将intellij配置为使用Eclipse编译器,但这可能不是原因。

    让我们用编译它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
    

    我们可以看到前额编译成一个程序:

    • 使用创建迭代器List.iterator()
    • 如果Iterator.hasNext():调用Iterator.next()继续循环

    至于“为什么这个无用的循环不能从编译的代码中得到优化?”我们可以看到,它对列表项没有任何作用:“好吧,你可以对你的iterable进行编码,这样.iterator()有副作用,或者说.hasNext()有副作用或有意义的后果。

    可以很容易地想象,表示数据库中可滚动查询的iterable可能会在HasNeXT()(比如联系数据库,或者由于已经到达结果集的末尾而关闭光标)。

    所以,即使我们能证明在环体中什么都没有发生,它还是更昂贵(难处理?)证明当我们迭代时没有任何有意义的/结果性的事情发生。编译器必须将这个空循环体留在程序中。

    我们最希望的是一个编译器警告. 有趣的是javac -Xlint:all Whatever.java警告我们这个空循环体。不过,Intellij的想法确实如此。诚然,我已经将intellij配置为使用Eclipse编译器,但这可能不是原因。

    enter image description here

        7
  •  -8
  •   Chandan    13 年前

    在处理集合时,应避免使用传统的for循环。 我将给出的简单原因是for循环的复杂度是O(sqr(n)),迭代器的复杂度甚至增强的for循环也只是O(n)。 所以它有一个性能上的区别。 只需拿一份1000件物品的清单,用两种方法打印出来。并打印执行的时差。你可以看到区别。