本文基於《Effective java》第58條
在此基礎之上加入了自己的理解。
for迴圈是平時寫程式碼用的最多的,但是之前看《Effective java》,大佬在某些場景寫並不推薦。結合著自己之前刷演算法題的經歷,受益匪淺。
一、for迴圈的缺點在以往遍歷元素的時候,我們通常採用以下的形式:
問題1:迭代器或索引多次出現,容易造成使用錯誤從上面兩種遍歷的程式碼上來看,對於陣列元素是透過索引i來遍歷的,但是整個for迴圈出現了四次i,對於物件元素是透過迭代器it來遍歷的,但是整個for迴圈出現了三次it。在for迴圈遍歷元素的時候,就有多次機會使用了錯誤的變數。而且有時候這些錯誤編譯器無法發現。對整個應用系統造成無法預知的錯誤。
問題2:遍歷物件元素時,需要注意容器型別比如我們這裡使用的是list,當然還有可能是其他容器型別,這些型別在更改時比較麻煩。
問題3:巢狀迭代丟擲異常這種情況比較複雜一些,先來搞個例子。比如說,我們想要列舉每種花,這些花有兩種屬性一種是顏色,一種是大小。
public class Main { //列舉顏色和尺寸 enum Color { RED, GREEN, BLUE, BLACK } enum Size { ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,NINE, TEN} //定義花 static class Flower{ public Flower(Color color, Size size) {} } public static void main(String[] args) { Collection<Color> colors = Arrays.asList(Color.values()); Collection<Size> sizes = Arrays.asList(Size.values()); List<Flower> flowers = new ArrayList<>(); //for迴圈新增所有的花和尺寸 for (Iterator<Color> color = colors.iterator(); color.hasNext(); ) { for (Iterator<Size> size = sizes.iterator(); size.hasNext(); ) { flowers.add(new Flower(color.next(), size.next())); } } }}
看似人畜無害,現在我們執行一波。
Exception in thread "main" java.util.NoSuchElementException at java.util.AbstractList$Itr.next(Unknown Source) at com.f2.Main.main(Main.java:25)
是不是感覺有點奇怪,好像雙重迴圈遍歷沒啥問題,但是出現了異常,原因是外部的Color迭代器呼叫了多次,第一層for迴圈被呼叫了,但是又在第二層for迴圈內部被呼叫了,所以color的next被呼叫完了。所以出現了NoSuchElementException。但是有時候也不會出現這種情況,場景是外部迴圈迭代器呼叫的次數剛好是內部呼叫的n倍。
問題4:巢狀迭代不是異常,但是結果不正確這種情況是外部迴圈迭代器呼叫的次數剛好是內部呼叫的n倍。我們再來個例子:
public class Main { //列舉顏色 enum Color { RED, GREEN, BLUE, BLACK } public static void main(String[] args) { Collection<Color> colors = Arrays.asList(Color.values()); //兩層for迴圈 for (Iterator<Color> c1 = colors.iterator(); c1.hasNext(); ) { for (Iterator<Color> c2 = colors.iterator(); c2.hasNext(); ) { System.out.println(c1.next()+" "+c2.next()); } } }}
現在對顏色進行for迴圈遍歷,一共兩層for迴圈,因為一共有四種顏色,兩層for迴圈應該是列印16個結果。現在執行一遍看看結果:
RED REDGREEN GREENBLUE BLUEBLACK BLACK
沒錯,確實是列印了四條。原因和問題三是一樣的。有一種方式可以很好地解決這種巢狀的問題。
巢狀迭代問題解決:直接看程式碼。既然是外部的迭代器it在內部使用了,那我在內部和外部之間用一個變數快取起來不就好了。
public class Main { //列舉顏色 enum Color { RED, GREEN, BLUE, BLACK } public static void main(String[] args) { Collection<Color> colors = Arrays.asList(Color.values()); //for迴圈 for (Iterator<Color> c1 = colors.iterator(); c1.hasNext(); ) { //用一個變數快取起來 Color c = c1.next(); for (Iterator<Color> c2 = colors.iterator(); c2.hasNext(); ) { System.out.println(c+" "+c2.next()); } } }}
現在再來執行,就可以很好地得出16種結果了。這種方式也比較不錯,但是卻不能很好地解決問題1和問題2。因此,為了解決這一現象,大佬Joshua Bloch在書中提出,推薦使用for-each迴圈來代替for迴圈。
二、for-each迴圈既然作者推薦使用for-each迴圈,我們看看他有什麼好處。是如何解決上面的問題的。
public class Main { //列舉顏色和尺寸 enum Color { RED, GREEN, BLUE, BLACK } enum Size { ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,NINE, TEN} //定義花 static class Flower{ public Flower(Color color, Size size) {} } public static void main(String[] args) { Collection<Color> colors = Arrays.asList(Color.values()); Collection<Size> sizes = Arrays.asList(Size.values()); List<Flower> flowers = new ArrayList<>(); //for-each迴圈 for (Color color:colors) { for (Size size:sizes ) { flowers.add(new Flower(color, size)); } } }}
看裡面的for-each迴圈。上面的問題就全都解決了。好吧,可能你會感覺,就這?還有一個好處還沒說,再往下看。
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 迴圈的:
(2)轉換:如果需要遍歷一個列表或陣列並替換其元素的部分或全部值,那麼需要列表迭代器或陣列索引來替換元素的值。
(3)並行迭代:如果需要並行地遍歷多個集合,那麼需要顯式地控制迭代器或索引變數,以便所有迭代器或索引變數都可以同步進行 。
如果發現自己處於這些情況中的任何一種,請使用傳統的 for 迴圈,並警惕本條目中提到的陷阱 。