如第45条所述,有些任务最好结合Stream来完成,有些最好结合迭代完成。下面是用一个传统的for循环遍历集合的例子:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
用传统的for循环遍历数组的做法如下:
// Not the best way to iterate over an array!
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
这些做法都比while循环(详见第57条)更好,但是它们并不完美。迭代器和索引变量都会造成一些混乱,而你需要的只是元素而已。而且,它们也代表着出错的可能。迭代器在每个循环中出现三次,索引变量在每个循环中出现四次,其中有两次让你很容易出错。一旦出错,就无法保证编译器能够发现错误。最后一点是,这两个循环是截然不同的,容器的类型转换了不必要的注意力,并且为了修改该类型增加了一些困难。
for-each循环(官方称之为增强的for语句)解决了所有的问题。通过完成隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组,同时简化了将容器的实现类型从一种转换到另一种的过程:
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
当见到冒号(:)时,可以把它读作在.....里面。因此上面的循环可以读作对于元素elements中的每一个元素e。注意,利用for-each循环不会有性能损失,甚至用于数组也一样:它们产生的代码本质上与手工编写的一样。
对于嵌套式迭代,for-each循环相对于传统for循环的优势还会更加明显。下面就是人们在试图对两个集合进行嵌套迭代时经常会犯的错误:
// Can you spot the bug?
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING } ...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
如果之前没有发现这个bug也不难过。许多专家级程序员偶尔也会犯这样的错误。问题在于,在迭代器上对外部的集合调用了太多次next方法。它应该从外部的循环进行调用,以便每种花色调用一次,但它却是从内部循环调用,因此每张牌调用一次。在用完所有的花色之后,循环就会抛出NoSuchElementException异常。
如果真的那么不幸,并且外部集合的大小是内部集合大小的几倍(可能是因为它们是相同的集合),循环就会正常终止,但是不会完成你想要的工作。例如,下面就是一个考虑不周的尝试,想要打印一对骰子的所有可能的滚法:
// Same bug, different symptom!
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX } ...
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
这个程序不会抛出异常,而是只打印6个重复的词(从“ONE ONE“ 到 ”SIX SIX“),而不是预计的那36种组合。
为了修正这些示例中的bug,必须在外部循环的作用域中添加一个变量来保存外部元素:
// Fixed, but ugly - you can do better!
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(suit, j.next()));
}
如果使用的是嵌套式for-each循环,这个问题就会完全消失。产生的代码将如你所希望的那样简洁:
// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
遗憾的是,有三种常见的情况无法使用for-each循环:
1、解构过滤
:如果需要变量集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。使用Java8中增加的Collection的removeIf方法,常常可以避免显式的遍历。
2、转换
:如果需要遍历列表或者数组,并取代它的部分或者全部原始值,就需要列表迭代器或者数组索引,以便设定元素的值。
平行迭代
:如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进(就如上述有问题的牌和骰子的示例中无意间所示范的那样)。
如果你发现自己处于以上任何一种情况之下,就要使用普通的for循环,并且要警惕本条目提到的陷阱。
for-each循环不仅能变量集合和数组,还能遍历实现Iterable接口的任何对象,该接口只包含单个方法,具体如下:
public interface Iterable<E> {
// Returns an iterator over the elements in this iterable
Iterator<E> iterator();
}
如果不得不从头开始编写自己的Iterator实现,其中还是有些技巧,但是如果编写的是表示一组元素的类型,则应该坚决考虑让它实现 Iterable接口,甚至可以选择让它不要实现Collection接口。这样,你的用户就可以利用for-each循环遍历类型,它们会永远心怀感激的。
总而言之,与传统的for循环相比,for-each循环在简洁、灵活性以及出现预防性方面都占有绝对优势,并且没有性能惩罚的问题。因此,当可以选择的时候,for-each循环应该优先于for循环。