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循環。

相關文章
相關標籤/搜索