【问题标题】:Which of these methods is most effective when I traverse a List?当我遍历 List 时,这些方法中哪一种最有效?
【发布时间】:2011-03-15 05:18:40
【问题描述】:
List<T> list = new ArrayList<T>();

1 种方法:

for(int i = list.length - 1; i >= 0; i--) {
  System.out.println(list.get(i));
}

2方法:

for(T t : list) {
  System.out.println(t);
}

3种方法:

Iterator<T> it = list.iterator();     
while(it.hasNext()) {
  System.out.println(it.next());
}

【问题讨论】:

  • “有效”是什么意思?您追求的是性能、可读性还是最佳实践?除非您知道存在问题,否则我肯定更喜欢优化简单性而不是性能。
  • int i = list.length - 1 应该是 int i = list.size() - 1。您的语法对于数组是正确的。
  • 请注意,尝试以反向顺序访问数组可能比以正向顺序更慢。将递减与比较相结合的好处是节省了一条指令,但一段时间以来,仅寄存器指令非常便宜。硬件缓存和内存读取针对前向读取进行了优化。此外,Oracle HotSpot,可能还有其他的,没有针对反向迭代的等效优化,因为它们很少使用(并且使用它们作为优化尝试的程序几乎肯定有其他更大的问题)。

标签: java performance


【解决方案1】:

效率不太可能显着 - 当然System.out.println 更有可能成为您特定示例中的瓶颈。

不过,第二种方法(增强的 for 循环)是最可读的。请注意,这三种方法的作用不同——第一种方法将从 end 而不是 start 进行迭代。获得正确的行为几乎总是胜过以极快的速度运行。您的代码越易读,就越有可能正确。

追求可读性,衡量您应用的性能,如果成为问题,则对瓶颈进行微优化(在每一步继续衡量)。

编辑:我是根据问题的第一行来回答这个问题的,显示正在使用ArrayList&lt;T&gt;

如果您想要 any List&lt;T&gt; 的答案,那么根本就没有准确的答案。 List&lt;T&gt; 不提供任何关于复杂性的保证。它没有说你应该期望get 有多快,也没有说你应该期望迭代器有多快。您可以使用具有良好随机访问的实现,但迭代器速度非常慢。这不太可能,但它仍然是有效的List&lt;T&gt;

简而言之:如果您关心效率,则需要知道您使用的是哪种列表。 一般迭代它可能是相当有效的,而随机访问可能会或可能不会有效。 (它可能比迭代更有效,但不太可能显着更有效。)在实践中,我的经验是,在大多数应用程序中,您实际上有足够的知识来做出合理的判断......但是我仍然通常会先编写代码以提高可读性。

正如其他人指出的那样,如果您确实想要使用随机访问来获取元素,那么确保您的列表也实现 RandomAccess 接口是值得的。

【讨论】:

  • 请参阅@jarnbjo 提供的答案,了解对否决票的解释。
  • @Tim:尽管问题的第一行?当然,这里至少存在一定程度的歧义......
  • 我投了反对票。感谢您的编辑。我认为可以安全地假设 OP 没有意识到他帖子中第一行的重要性并且问题指定了 List。
【解决方案2】:

我更喜欢方法 2,除非你需要在迭代器对象上调用某些东西(如 iterator.remove()),在这种情况下我更喜欢方法 3。

我会使用方法 1 的唯一情况是,如果我需要跟踪循环中的索引(然后仅在 List 的情况下,这是一个密集的类似数组的实现,比如ArrayList)。

更新: 根据 cmets/其他答案,我现在更喜欢 ListIterator 在遍历 List 以跟踪索引时。离开方法 1 只对通过任何其他方法都无法实现的事情有用。

【讨论】:

  • 查看@jarnbjo 提供的答案,了解对否决票的解释。
  • 如果您想跟踪索引,请考虑 ListIterator。看我的回答。
  • 好点,不是我以前用过的东西,但绝对比旧式 for 循环更喜欢它。
【解决方案3】:

我发现this比较也许它可以帮助你转发

【讨论】:

    【解决方案4】:

    如果您的列表很长而没有随机访问支持,第一种方法会慢得多。例如。在 LinkedList 上调用 get(1000000) 必须从列表的开头循环遍历 1000000 个元素才能到达第 1000000 个元素。第二种和第三种方法应该是等价的。

    如果它与您的情况相关,提供对其元素的恒定时间随机访问的 List 实现应该实现 java.util.RandomAccess 标记接口。至少标准 API 中的 List 实现可以。

    【讨论】:

    • 问题本身(“......当我想遍历一个列表?”)在标题中,并没有指定 Chris 是指哪个 List 实现。
    • @jarnbjo:所以问题的所有相关信息都必须在标题中?
    • @skeet:我在哪里写了一个问题必须在标题中?在这种情况下,它是:“当我想遍历列表时,哪种方法最有效?”你是对的,示例代码实际上使用了一个 ArrayList,但这不是 Chris 所问的。
    • @jarbjo:我认为在回答问题时考虑提供的代码是合理的,仅此而已......(我并不是说你写的任何东西都是错误的,请注意。 )
    • 不知道作者有没有意识到LinkedList和ArrayList的这种区别...
    【解决方案5】:

    许多因素会影响测量。

    首先,请注意您的第二个和第三个替代方案是字节码等效的。实际上,Java enhanced for loop 是一种语法糖,这意味着,在您的情况下,第二个替代方案在编译后看起来完全类似于您的第三个替代方案。

    最明显的一个是选择的实现。如果您使用LinkedList 而不是ArrayList 进行此测试,您会发现,很明显,迭代器比随机数组访问要快。您甚至会发现,在某些情况下,迭代 LinkedList 比迭代 ArrayList 更快。你现在呢?在您的大部分代码中,您会无缘无故地从一个实现切换到另一个实现。

    因此,作为一般经验法则,为了可读性和代码效率,我倾向于始终使用第二种方法。

    【讨论】:

    • 第二种和第三种方法实际上并没有生成等效的字节码。对局部变量 t 的临时赋值(在第二种方法中)将在编译后的字节码中保持原样,而为第三种方法生成的字节码会将 it.next() 的结果留在堆栈上并直接用作println 调用的参数。
    【解决方案6】:

    开启RandomAccess

    java.util.ArrayList 实现RandomAccess。文档清楚地说明了这意味着什么:

    List 实现使用的标记接口表明它们支持快速(通常是恒定时间)随机访问。 [...] 根据经验,List 实现应该实现此接口,如果对于该类的典型实例,此循环:

    for (int i=0, n=list.size(); i < n; i++)
         list.get(i);
    

    比这个循环运行得更快:

    for (Iterator i=list.iterator(); i.hasNext(); )
         i.next();
    

    因此,对于 正确 implements RandomAccessList,索引-get 更快。

    但是请注意,对于LinkedList 来说,这是正确的,其中上面的代码表现出二次性能。这是因为LinkedList 不允许恒定时间随机访问;也就是说,get 对于LinkedList 是线性的。

    请注意,即使索引-get 更快,它也只是一个常数因子。首先分析一下这种优化尝试是否值得。

    相关问题


    for-eachIterator 循环上

    您应该关注的这两种构造的性能之间没有显着差异。 for-each 循环更具可读性,但适用于更有限的场景,因此在这些场景中正是您应该使用它们的地方。当for-each 循环不适用时,请使用Iterator 循环。

    引自Effective Java 2nd Edition,Item 46:Prefer for-each loops to traditional for loops

    1.5 版中引入的for-each 循环消除了混乱 以及隐藏迭代器或索引变量的错误机会 完全地。由此产生的习语同样适用于集合和数组:

    // The preferred idiom for iterating over collections and arrays
    for (Element e : elements) {
        doSomething(e);
    }
    

    当您看到冒号 (:) 时,将其读作“in”。因此,上面的循环读作 “对于元素中的每个元素e。”请注意,没有性能损失 使用 for-each 循环,即使是数组。事实上,它可能会提供轻微的 在某些情况下,性能优于普通的 for 循环,因为它 只计算一次数组索引的限制。

    另见

    相关问题

    【讨论】:

    • 从哪个Java版本开始,引入了这个RandomAccess Marker Interface?
    • @Bragboy:文档说 1.4。 @since 是标准的 javadoc 标记。
    【解决方案7】:

    方法 1 不太可能是最好的方法,因为它与其他方法相比没有可读性或性能优势。

    如果您想跟踪索引,请考虑使用ListIterator 而不是Iterator,例如

    ListIterator<T> it = list.listIterator();
    while (it.hasNext()) {
        T t = it.next;
        int index = it.previousIndex();
        System.out.printf("%d => %s%n", index, t);
    }
    

    正如其他人所指出的,除非您想在迭代期间从列表中删除元素,否则方法 3 等效于方法 2。

    如果分析显示列表的 get(int) 方法或迭代器的 next()hasNext() 是性能瓶颈(非常罕见但可能会发生),请考虑将列表替换为数组(您仍然可以使用 for -each 语法,类似于方法2。)

    【讨论】:

      【解决方案8】:

      由于没有解释有效性究竟是什么意思,我将从应该首选的位置来回答。

      我更喜欢第二种方法可读性最强

      为什么:在编写程序之前,您不知道瓶颈会在哪里。这就是过早优化不太可能产生实质性结果的原因。但是代码的可读性总是相关的,所以我会选择第二种变体。

      【讨论】:

        【解决方案9】:

        如果你关心性能,它取决于 List 的实现。 使用ArrayList,第一个是最快的。它和以下一样快:

        for(int i = 0, n=list.size(); i < n; i++) {
          System.out.println(list.get(i));
        }
        

        那是因为ArrayList 在里面缓冲了一个数组。当你调用objArrayList.get(i)时,它会返回objArrayList.buffArray[i]

        对于each和Iterator是一样的。因此同样慢

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-06-07
          • 2015-11-23
          • 1970-01-01
          • 2013-06-28
          • 1970-01-01
          • 2019-11-23
          • 1970-01-01
          • 2012-11-22
          相关资源
          最近更新 更多