Effective Java 第三版——58. for-each循环优于传统for循环

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java

Effective Java, Third Edition

58. for-each循环优于传统for循环

正如在条目 45中所讨论的,一些任务最好使用Stream来完成,一些任务最好使用迭代。下面是一个传统的for循环来遍历一个集合:git

// Not the best way to iterate over a collection!
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),可是它们并不完美。迭代器和索引变量都很混乱——你只须要元素而已。此外,它们也表明了出错的机会。迭代器在每一个循环中出现三次,索引变量出现四次,这使你有不少机会使用错误的变量。若是这样作,就不能保证编译器会发现到问题。最后,这两个循环很是不一样,引发了对容器类型的没必要要注意,而且增长了更改该类型的小麻烦。github

for-each循环(官方称为“加强的for语句”)解决了全部这些问题。它经过隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习惯用法一样适用于集合和数组,从而简化了将容器的实现类型从一种转换为另外一种的过程:数组

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

当看到冒号(:)时,请将其读做“in”。所以,上面的循环读做“对于元素elements中的每一个元素e”。“使用for-each循环不会下降性能,即便对于数组也是如此:它们生成的代码本质上与手工编写的代码相同。性能

当涉及到嵌套迭代时,for-each循环相对于传统for循环的优点甚至更大。下面是人们在进行嵌套迭代时常常犯的一个错误:ui

// 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,也没必要感到难过。许多专业程序员都曾犯过这样或那样的错误。问题是,对于外部集合(suit),next方法在迭代器上调用了太屡次。它应该从外部循环调用,所以每花色调用一次,但它是从内部循环调用的,所以每一张牌调用一次。在suit用完以后,循环抛出NoSuchElementException异常。this

若是你真的不走运,外部集合的大小是内部集合大小的倍数——也许它们是相同的集合——循环将正常终止,但它不会作你想要的。 例如,考虑这种错误的尝试,打印一对骰子的全部可能的掷法:code

// 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个组合。对象

要修复例子中的错误,必须在外部循环的做用域内添加一个变量来保存外部元素:

/ 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循环的:

  • 有损过滤(Destructive filtering)——若是须要遍历集合,并删除指定选元素,则须要使用显式迭代器,以即可以调用其remove方法。 一般可使用在Java 8中添加的Collection类中的removeIf方法,来避免显式遍历。
  • 转换——若是须要遍历一个列表或数组并替换其元素的部分或所有值,那么须要列表迭代器或数组索引来替换元素的值。

  • 并行迭代——若是须要并行地遍历多个集合,那么须要显式地控制迭代器或索引变量,以便全部迭代器或索引变量均可以同步进行(正如上面错误的card和dice示例中无心中演示的那样)。

若是发现本身处于这些状况中的任何一种,请使用传统的for循环,并警戒本条目中提到的陷阱。

for-each循环不只容许遍历集合和数组,还容许遍历实现Iterable接口的任何对象,该接口由单个方法组成。接口定义以下:

public interface Iterable<E> {
    // Returns an iterator over the elements in this iterable
    Iterator<E> iterator();
}

若是必须从头开始编写本身的Iterator实现,那么实现Iterable会有点棘手,可是若是你正在编写表示一组元素的类型,那么你应该强烈考虑让它实现Iterable接口,甚至能够选择不让它实现Collection接口。这容许用户使用for-each循环遍历类型,他们会永远感激涕零的。

总之,for-each循环在清晰度,灵活性和错误预防方面提供了超越传统for循环的使人注目的优点,并且没有性能损失。 尽量使用for-each循环优先于for循环。

相关文章
相关标签/搜索