for-each与for与while的区别

6

我想知道在ArrayList或任何类型的List上实现“for-each”循环的最佳方法是什么。

以下哪种实现方式是最好的,为什么?还是有更好的方法吗?

谢谢您的帮助。


List values = new ArrayList();

values.add("one"); values.add("two"); values.add("three"); ...

//使用增强for循环 for(String value : values) { ... }

//使用普通for循环 for(int i = 0; i < values.size(); i++) { String value = values.get(i); ... }

//使用迭代器 for(Iterator it = values.iterator(); it.hasNext(); ) { String value = it.next(); ... }

//使用while循环和迭代器 Iterator it = values.iterator(); while (it.hasNext()) { String value = (String) it.next(); ... }


可能相关:https://dev59.com/snVD5IYBdhLWcg3wE3Ro请仅返回翻译后的文本。 - Ben Voigt
4个回答

21

#3的缺点在于迭代器it的范围超出了循环的结尾。其他解决方案没有这个问题。

#2与#0完全相同,只是#0更易读且更不容易出错。

#1可能效率较低,因为它每次通过循环都调用.size()方法。

#0通常是最佳选择,因为:

  • 它最短
  • 最不容易出错
  • 符合惯用法,其他人可以一眼看懂
  • 编译器可以有效地实现它
  • 它不会在方法作用域(循环外部)中污染不必要的名称

不错的回答。然而,在更新时需要使用#1(如果不仅仅是改变当前项或构建新列表的结果),并且带有索引。由于在这种情况下List<>是一个ArrayList<>,所以get()(和size())是O(1),但对于所有List-contract类型来说都不是这样。 - user166390
#2 是在循环过程中需要移除元素时使用的解决方案。 - Naxos84

2
简短的答案是使用版本0。请查看Android针对性能设计的文档中标题为“使用增强的for循环语法”的章节。该页面有许多好东西,而且非常清晰简洁。

这是针对Android的,但在这个问题中没有Android标签,而且最终取决于你需要什么。 - Jorge Aguilar

1

#0 在我看来是最容易阅读的,但 #2 和 #3 也同样有效。这三者之间不应该有性能差异。

在几乎任何情况下,都不应该使用 #1。你在问题中提到可能需要迭代“每种类型的列表”。如果你恰好正在迭代一个 LinkedList,那么 #1 的复杂度将会是 n^2:不好。即使你绝对确定你正在使用支持高效随机访问的列表(例如 ArrayList),通常也没有理由使用 #1 而不是其他任何一种方法。


“#1永远不应该被使用”这种说法并不正确。它是唯一一种可以更改传递的List中单元格的方法,尽管必须考虑时间复杂度(这在List中并未暗示)。 - user166390
改变单元格?你的意思是什么? - Cameron Skinner
对于(int i = 0; i < x.size(); i++){x.set(i, i * 2);} 这是一个愚蠢的例子,在许多情况下,只需创建新列表即可更好。此外,在这种情况下限制为ArrayList可能更合适。但这确实是对“永不”的反驳。 - user166390
好的,很公平。虽然我仍然建议优先使用ListIterator及其set方法。这样它仍然可以有效地处理非随机访问列表。 - Cameron Skinner
我稍微放宽了"绝不"的语气 :) - Cameron Skinner

1

针对OP的评论,我们来看一下这些问题:

然而,在更新时(如果不仅仅是改变当前项或构建新列表),#1是必需的,并且伴随着索引。由于在这种情况下List<>是ArrayList<>,因此get()(和size())是O(1),但并非所有List-contract类型都是如此。

让我们来看看这些问题:

确实,对于所有实现了List接口的类,get(int)并不是O(1)。然而,据我所知,java.util中所有List实现的size()都是O(1)的。但你说得对,对于许多List实现,#1方法是次优的。事实上,对于像LinkedList这样的列表,其中get(int)O(N)的,#1方法会导致O(N^2)的列表迭代。

ArrayList 的情况下,手动将调用 size() 提升为 (final) 本地变量是一件简单的事情。通过这种优化,#1 代码比其他情况...对于 ArrayList 来说,速度显著更快。
关于在迭代元素时更改列表的观点引发了许多问题:
  • 如果您使用明确或隐式使用迭代器的解决方案,则根据列表类,您可能会收到ConcurrentModificationException。如果您使用并发集合类之一,则不会收到异常,但是javadoc指出迭代器不一定会返回所有列表元素。

  • 如果您使用#1代码(原样)进行此操作,则存在问题。如果修改由同一线程执行,则需要调整索引变量以避免丢失条目或将它们返回两次。即使您做得完全正确,在当前位置之前同时插入的列表条目也不会显示出来。

  • 如果#1情况下的修改由不同的线程执行,则很难正确同步。核心问题在于get(int)size()是单独的操作。即使它们分别同步,也没有阻止其他线程在sizeget调用之间修改列表。

简而言之,迭代正在同时修改的列表很棘手,应该避免... 除非您真的知道自己在做什么


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