1. 程式人生 > >Effective Java 第三版——58. for-each循環優於傳統for循環

Effective Java 第三版——58. for-each循環優於傳統for循環

inter clu its 程序員 方法 討論 one 控制 傳統

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
註意,書中的有些代碼裏方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

技術分享圖片

58. for-each循環優於傳統for循環

正如在條目 45中所討論的,一些任務最好使用Stream來完成,一些任務最好使用叠代。下面是一個傳統的for循環來遍歷一個集合:

// 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),但是它們並不完美。叠代器和索引變量都很混亂——你只需要元素而已。此外,它們也代表了出錯的機會。叠代器在每個循環中出現三次,索引變量出現四次,這使你有很多機會使用錯誤的變量。如果這樣做,就不能保證編譯器會發現到問題。最後,這兩個循環非常不同,引起了對容器類型的不必要註意,並且增加了更改該類型的小麻煩。

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循環的優勢甚至更大。下面是人們在進行嵌套叠代時經常犯的一個錯誤:

// 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異常。

如果你真的不走運,外部集合的大小是內部集合大小的倍數——也許它們是相同的集合——循環將正常終止,但它不會做你想要的。 例如,考慮這種錯誤的嘗試,打印一對骰子的所有可能的擲法:

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

Effective Java 第三版——58. for-each循環優於傳統for循環