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

时间:2010-01-21 21:52:10

标签: java collections foreach

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

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)的循环,而不是使用迭代器。

根据Meta的建议,我将发布我对此问题的回答。

7 个答案:

答案 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之类的操作。

e.g。

没关系:

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

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

  1. 即使列表为空(Iterator<CustomObj> iter = customList.iterator(););
  2. ,也要分配对象
  3. iter.hasNext()在循环的每次迭代中都有一个invokeInterface虚拟调用(遍历所有类,然后在跳转之前执行方法表查找)。
  4. 迭代器的实现必须至少进行2次字段查找才能使hasNext()调用数值:#1获取当前计数,#2获取总计数
  5. 在body循环内部,还有另一个invokeInterface虚拟调用iter.next(所以:遍历所有类并在跳转之前进行方法表查找)并且还必须进行字段查找:#1获取索引和#2获取对数组的引用以进行偏移(在每次迭代中)。
  6. 潜在的优化是使用缓存的大小查找切换到index iteration

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

    我们有:

    1. 一个invokeInterface虚拟方法调用customList.size()初始创建for循环以获取大小
    2. 在body for循环期间调用get方法调用customList.get(x),这是对数组的字段查找,然后可以将偏移量放入数组中
    3. 我们减少了大量的方法调用,字段查找。您不希望使用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编译器,但这可能不是原因。

enter image description here

答案 6 :(得分:-8)

在使用Collections时,我们应该避免使用传统的for循环。 我将给出的简单原因是for循环的复杂性是O(sqr(n))的顺序,Iterator的复杂性甚至增强的for循环只是O(n)。 所以它给出了一个性能差异.. 只需列出大约1000个项目并使用两种方式打印。并打印执行的时差。你可以看到差异。