如第45条所述,有些任务最好结合Stream来玩完成,有些最好结合迭代器完成。下面是用一个传统的for循环遍历集合的例子:
for(Iterator<Element> i = c.iterator(); i.hasNext();){
Element e = i.next();
}
用传统的for循环遍历数组的做法如下:
for(int i = 0; i < a.length; i++){
}
这些做法逗比while循环更好,但是它们并不完美。迭代器和索引变量都会造成一些混乱——二你需要的只是元素而已。而且,它们也代表着出错的可能。迭代器在每个循环中出现三次,索引变量在每个循环中出现四次,期中两次让你很容易错误。一但出错,就无法保证编译器能够发现错误。最后一点,这两个循环是截然不同的,容器类型转移了不必要的注意力,并且为修改该类型增加了一些困难。
foreach循环(官方称之为“增强的for语句”)解决了所有问题。通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组,同时简化了将容器的实现类型从一种转换到另一种的过程:
for(Element e : elemets){
}
当见到冒号(:)时,可以把它读作“在......里面”。因此上面的循环可以读作“对于元素elements中的每一个元素e”。注意,利用foreach循环不会有性能损失,甚至用于数组也一样:它们产生的代码本质上与手工编写的一样。
对于嵌套式迭代,foreach循环相对于传统的for循环的优势还会更加明显。下面就是人们在试图对两个集合进行嵌套迭代时经常会犯的错误:
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也不必难过。许多专家级的程序员偶尔也会犯这样的错误。问题在于,在迭代器上对外部的集合(suits)调用了太多次next方法。它应该从外部的循环中进行调用,以便每种花色调用一次,但它确实从内部循环调用,因此每张牌调用一次。在用完所有花色之后,循环就会抛出NoSuchElementException异常。
如果真的那么不幸,并且外部集合的大小是内部集合大小的几倍(可能因为它们是相同的集合),循环就会正常终止,但是不会完成你想要的工作。例如,下面就是一个考虑不周的尝试,想要打印一堆骰子的所有可能的滚法:
enum Face { ONE, TOW, 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, 必须在外部循环的作用域中添加一个变量来保存外部元素:
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循环,这个问题就会完全消失。产生的代码将如你所希望的那样简洁:
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
遗憾的是,有三种常见的情况无法使用foreach循环:
- 解构过滤——如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。使用Java 8中增加的Collection的removeIf方法,常常可以避免显示的遍历。
- 转换——如果需要遍历列表或者数组,并取代它们的部分或者全部元素值,就需要列表迭代器或者数组索引,以便设定元素值。
- 平行迭代——-如果需要并行遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进(就如上述有问题地牌和骰子地示例中无意间所示范地那样)。
如果你发现自己处于以上任何一种情况之下,就要使用普通地for循环,并且要警惕本条目中提到地陷阱。
for-each循环不仅仅能够遍历集合数组,还能遍历实现Iterable接口地任何对象,该接口中只包含单个方法,具体如下所示:
如果不得不从头编写自己地Iterator实现,其中还是有些技巧地,但是如果编写的是表示一组元素的类型,则应该坚决考虑让它实现Itertor接口,甚至可以选择让它不要实现Collection接口。这样,你的用户就可以利用for-each循环遍历类型。
总而言之,与传统的for循环相比,foreach循环在简洁性、灵活性以及出错防御性方面都有着绝对优势,并且没有性能惩罚的问题。因此,当可以选择的时候,foreach循环应该优先于for循环。
public interface It