Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
正如在條目 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循環的:
轉換——若是須要遍歷一個列表或數組並替換其元素的部分或所有值,那麼須要列表迭代器或數組索引來替換元素的值。
並行迭代——若是須要並行地遍歷多個集合,那麼須要顯式地控制迭代器或索引變量,以便全部迭代器或索引變量均可以同步進行(正如上面錯誤的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循環。