为什么增强型for循环比普通for循环更高效

33
我在这里读到增强for循环比普通的for循环更有效率:

http://developer.android.com/guide/practices/performance.html#foreach

当我搜索它们效率的差异时,发现的唯一不同之处是:在普通的for循环中,我们需要额外的步骤来找出数组或大小的长度等信息。

for(Integer i : list){
   ....
}


int n = list.size();
for(int i=0; i < n; ++i){
  ....
}

但这是增强型for循环比普通for循环好的唯一原因吗?在这种情况下,最好使用普通的“for循环”来理解增强的“for循环”,因为它略微复杂。

查看此处以获取有趣的问题:http://www.coderanch.com/t/258147/java-programmer-SCJP/certification/Enhanced-Loop-Vs-Loop

能否有人请解释这两种类型的for循环的内部实现,或者解释使用增强的“for循环”的其他原因?


4
如果你在某处“读到了什么内容”,最好在可能的情况下进行引用。 - cheeken
我在这里阅读了它...但是没有任何解释为什么会这样:http://developer.android.com/guide/practices/performance.html#foreach - Archie.bpgc
2
总之:默认情况下使用增强型for循环,但对于性能关键的ArrayList迭代,请考虑手写计数循环。这与您所读到的相反:对于ArrayList来说,第二种选择更快。 - Thilo
@Archie.bpgc:实际上,有一个解释:“zero()是最慢的,因为JIT还不能优化掉每次迭代循环时获取数组长度的开销。” 对我来说,这看起来像是一个解释。 - Joachim Sauer
留下 zero() ... 这是最糟糕的循环方式 ... 但是 one() 呢?我的问题是,在数组循环中,one() 和 two() 没有区别(以 Java 为例)... 但在 Android 中,即使是对于数组,我最好也使用 two() 吗? - Archie.bpgc
该链接文章讨论了数组。在数组上的posh for循环与列表(可迭代对象)上的for循环完全不同。 - Tom Hawtin - tackline
7个回答

58

简单地说,使用增强for循环更高效有些过于简化了。它 可能 会更高效,但在许多情况下,它几乎与传统的循环相同。

首先需要注意的是,对于集合,增强的for循环使用Iterator,因此如果手动使用Iterator迭代集合,则与增强的for循环相比,性能几乎相同。

增强for循环比天真实现的传统循环更快的一个场景是这样的:

LinkedList<Object> list = ...;

// Loop 1:
int size = list.size();
for (int i = 0; i<size; i++) {
   Object o = list.get(i);
   /// do stuff
}

// Loop 2:
for (Object o : list) {
  // do stuff
}

// Loop 3:
Iterator<Object> it = list.iterator();
while (it.hasNext()) {
  Object o = it.next();
  // do stuff
}
在这种情况下,循环1的速度将比循环2和3都要慢,因为它每次迭代都需要(部分地)遍历列表以找到位置为i的元素。然而,由于使用了Iterator,循环2和3只会在列表中向前移动一个元素。循环2和3的性能也几乎相同,因为循环3基本上就是编译器在你用循环2编写代码时产生的结果。

3
在所引用的例子中,“随机访问”和“顺序访问”集合之间的区别最终似乎是显而易见的。对于数组来说,这两种循环类型的行为都是相同的。 - Santosh
1
@Archie.bpgc:首先,Android与“Java”不同。Dalvik VM在优化方面还不如HotSpot VM,因此在某些情况下,Android的性能建议与“普通”的Java有所不同。这是其中一个例子。仔细阅读文档:特别是示例onetwothree,在“普通”的JVM上它们的性能可能相当相似。 - Joachim Sauer
1
@Archie.bpgc:是的,对于Android来说,示例three是数组的最佳选择。对于ArrayList(以及可能的其他随机访问List实现),这个指南告诉你不要使用增强型for循环(可能是为了避免Iterator分配开销)。 - Joachim Sauer
你再次说:“对于ArrayList(和可能是其他随机访问列表实现),这个指南告诉你不要使用增强的for循环(可能是为了避免迭代器分配开销)。”但是数组是一种随机访问的数据结构,对吧? - Archie.bpgc
@JoachimSauer,我不确定你的这个说法:“但是循环2和3只会在列表中向前移动一个元素,因为使用了迭代器”,因为如果我看迭代器的实现,它也做同样的事情,它也使用索引并调用列表的get方法,那么它与简单的FOR循环有什么不同呢? - hagrawal7777
显示剩余6条评论

12

今天我进行了一个小实验,使用上述的三种方法之一向链表中插入一定数量的元素,并通过以下三种方法之一迭代它:1)使用高级for循环;2)使用迭代器;3)使用简单的loop和get()方法。
我猜像你们这样的程序员通过看代码更能理解我做了什么。

long advanced_timeElapsed,iterating_timeElapsed,simple_timeElapsed;
long first=System.nanoTime();
for(Integer i: myList){
  Integer b=i;
}
long end= System.nanoTime();
advanced_timeElapsed=end-first;
System.out.println("Time for Advanced for loop:"+advanced_timeElapsed);

first=System.nanoTime();
Iterator<Integer> it = myList.iterator();
while(it.hasNext())
{
  Integer b=it.next();
}
end= System.nanoTime();
iterating_timeElapsed=end-first;
System.out.println("Time for Iterating Loop:"+iterating_timeElapsed);
        first=System.nanoTime();

int counter=0;
int size= myList.size();
while(counter<size)
{
  Integer b=myList.get(counter);
  counter++;    
}
end= System.nanoTime();
simple_timeElapsed=end-first;
System.out.println("Time for Simple Loop:"+simple_timeElapsed);

结果不如我预期。以下是三种情况下经过的时间的图表。 Time Elapsed Graph

纵轴表示经过的时间,横轴表示测试用例。

测试用例 输入
1 10个输入
2 30个输入
3 50个输入
4 100个输入
5 150个输入
6 300个输入
7 500个输入
8 1000个输入
9 2000个输入
10 5000个输入
11 10000个输入
12 100000个输入

你可以看到,简单的循环比其他方法表现得更好。如果在上面的代码中发现任何错误,请回复我,我会再次检查。在我深入了解字节码并查看底层情况后,我会进一步更新。
非常抱歉回复这么长,但我喜欢描述详细。
Philip


所以它归结为这种情况,1)增强型for循环只是迭代器的实现。它创建一个迭代器对象,执行迭代器的hasNext()并执行next()以获取下一个元素。因此,实际上它与情况2相同。但它无法解释增强型和迭代器之间图表中急剧上升的原因。在情况3中,由于在arraylist中,get(index)是对数组的随机访问,所以它应该表现更好。 - Philip George
3
你曾经重新排列过测试案例吗?第一次实现较慢的原因可能是由于缓存未命中(对象尚未在缓存中)。 - Siu Ching Pong -Asuka Kenji-
是的,事实上,我重新排序并尝试过了。我不知道JIT是否会影响基准测试。但当我窥视字节码时,它确实显示简单循环效果更好。 - Philip George
1
这取决于数据结构。如果您使用LinkedList,您将看到完全不同的情况。 - Adil Aliyev

7

我读到增强型for循环比普通for循环更高效。

实际上,有时候它的程序效率会低一些,但大多数情况下效率是完全相同的。

对于开发人员来说,这更有效率,而这通常更为重要。

当迭代集合时,for-each循环特别有用。

List<String> list = 
for(Iterator<String> iter = list.iterator(); list.hasNext(); ) {
    String s = list.next();

可以更容易地编写为(但与原来的代码执行相同,因此对程序效率没有影响)

List<String> list = 
for(String s: list) {

当通过索引访问随机访问集合时,使用“旧”循环略微更有效率。

List<String> list = new ArrayList<String>(); // implements RandomAccess
for(int i=0, len = list.size(); i < len; i++) // doesn't use an Iterator!

在集合上使用for-each循环总是使用迭代器,对于随机访问列表效率略低。

据我所知,使用for-each循环从未更有效率,但正如我所说,开发者的效率往往更为重要


你能否解释一下它们的实现方式(区别在哪里)或者举例说明一个方法比另一个更高效的情况吗? - Archie.bpgc
正如其他人在上面指出的那样,一个写得不好的老式for循环可能比新的for-each循环效率低。所以现在你知道了一个for-each循环比程序更有效的情况。 - Uncle Iroh
@UncleIroh 使用for-each循环意味着你不太可能编写一些低效的代码。但是,如果你知道自己在做什么,for-each生成的字节码与你自己编写的代码完全相同,只是一种简写形式。从字节码层面上看,没有办法确定两者之间的区别(除了生成的本地变量名称)。良好编写的代码在两种情况下都是相同的。如果你有一个ArrayList,使用索引for循环比使用for-each更高效,因为后者使用迭代器。 - Peter Lawrey

3

foreach循环与以下这种循环一样高效:

for (Iterator<Foo> it = list.iterator(); it.hasNext(); ) {
     Foo foo = it.next();
     ...
}

因为它是严格等价的。
如果您使用迭代来遍历列表
int size = list.size();
for (int i = 0; i < size; i++) {
    Foo foo = list.get(i);
    ...
}

然后,foreach循环将与您的循环具有相同的性能,但仅适用于ArrayList。在LinkedList的情况下,由于在每次迭代时,它必须遍历列表的所有节点直到达到第i个元素,您的循环将具有可怕的性能。
foreach循环(或基于迭代器的循环,这是相同的),没有这个问题,因为迭代器保留对当前节点的引用,并在每次迭代时简单地转到下一个节点。它是最好的选择,因为它可以很好地处理所有类型的列表。它还更清晰地表达了意图,而且更安全,因为您不会在循环内增加索引,在嵌套循环的情况下使用错误的索引。

你的意思是它们的效率相同,但在使用循环遍历链表或类似结构时...普通的for循环是递归遍历,而foreach循环是迭代遍历? - Archie.bpgc
2
这不是递归与迭代的问题。只是在链表上进行get()操作时,需要从第一个节点开始,获取下一个节点直到索引i。这是一个O(n)的操作,而在ArrayList的情况下是O(1)。从迭代器中获取下一个元素始终是O(1)的。无论列表是ArrayList还是LinkedList都没有关系:性能尽可能好。 - JB Nizet
如果你(大量)使用LinkedList,那么你的性能可能会非常糟糕。 - Tom Hawtin - tackline

3

for-each循环使用Iterator接口。我怀疑它比“旧”样式更有效率。Iterator还需要检查列表的大小。

这主要是为了可读性。

对于非随机访问集合(如LinkedList),它应该比较快,但这时比较是不公平的。你本来就不会使用第二种实现(具有缓慢的索引访问)。


1

所有答案都很好,我认为不需要更多答案,但我想指出:

增强型的for语句只能用于获取数组元素

不能用于修改元素。

如果程序需要修改元素,请使用传统的计数器控制for语句。

看一下

int[] array = { 1, 2, 3, 4, 5 };
for (int counter = 0; counter < array.length; counter++)
    array[counter] *= 2;

我们不能使用"增强for循环"语句,因为我们正在修改数组的元素。

0

来自《Effective Java》:

遍历数组的标准习惯用法不一定会导致冗余检查。现代JVM实现会将其优化掉。

但Josh Bloch并没有描述JVM如何进行优化。


当然他不会!这是一本关于如何编写良好的Java代码的教科书,而不是关于JVM内部的内容。要理解优化器如何执行此类操作,您需要查阅有关编译器编写的教科书。至于Java JIT编译器如何执行此操作的具体细节,请查看JVM源代码。 - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接