為什么推薦使用For-Each而不是For循環(huán)遍歷元素?
for循環(huán)是平時(shí)寫代碼用的最多的,但是之前看《Effective java》,大佬在某些場(chǎng)景寫并不推薦。結(jié)合著自己之前刷算法題的經(jīng)歷,受益匪淺。
一、for循環(huán)的缺點(diǎn)
在以往遍歷元素的時(shí)候,我們通常采用以下的形式:
- public class Main {
- public static void main(String[] args) {
- //1、數(shù)組元素
- int[] num = new int[] {1,2,3,4,5};
- //數(shù)組的遍歷
- for(int i=0;i<num.length;i++)
- System.out.println(num[i]);
- //2、對(duì)象元素
- ArrayList<Person> lists = new ArrayList<>();
- lists.add(new Person("張三"));
- lists.add(new Person("李四"));
- lists.add(new Person("愚公要移山"));
- //對(duì)象元素的遍歷
- for(Iterator<Person> it=lists.iterator();it.hasNext();) {
- Person p = it.next();
- System.out.println(p.getName());
- }
- }
- }
這種寫法看起來(lái)還不錯(cuò),但是卻并不完美。我們來(lái)分析一下,有什么缺點(diǎn)。然后給出解決方案。
問題1:迭代器或索引多次出現(xiàn),容易造成使用錯(cuò)誤
從上面兩種遍歷的代碼上來(lái)看,對(duì)于數(shù)組元素是通過索引i來(lái)遍歷的,但是整個(gè)for循環(huán)出現(xiàn)了四次i,對(duì)于對(duì)象元素是通過迭代器it來(lái)遍歷的,但是整個(gè)for循環(huán)出現(xiàn)了三次it。在for循環(huán)遍歷元素的時(shí)候,就有多次機(jī)會(huì)使用了錯(cuò)誤的變量。而且有時(shí)候這些錯(cuò)誤編譯器無(wú)法發(fā)現(xiàn)。對(duì)整個(gè)應(yīng)用系統(tǒng)造成無(wú)法預(yù)知的錯(cuò)誤。
問題2:遍歷對(duì)象元素時(shí),需要注意容器類型
比如我們這里使用的是list,當(dāng)然還有可能是其他容器類型,這些類型在更改時(shí)比較麻煩。
問題3:嵌套迭代拋出異常
這種情況比較復(fù)雜一些,先來(lái)搞個(gè)例子。比如說,我們想要列舉每種花,這些花有兩種屬性一種是顏色,一種是大小。
- 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循環(huán)添加所有的花和尺寸
- for (Iterator<Color> color = colors.iterator(); color.hasNext(); ) {
- for (Iterator<Size> size = sizes.iterator(); size.hasNext(); ) {
- flowers.add(new Flower(color.next(), size.next()));
- }
- }
- }
- }
看似人畜無(wú)害,現(xiàn)在我們運(yùn)行一波。
- Exception in thread "main" java.util.NoSuchElementException
- at java.util.AbstractList$Itr.next(Unknown Source)
- at com.f2.Main.main(Main.java:25)
是不是感覺有點(diǎn)奇怪,好像雙重循環(huán)遍歷沒啥問題,但是出現(xiàn)了異常,原因是外部的Color迭代器調(diào)用了多次,第一層for循環(huán)被調(diào)用了,但是又在第二層for循環(huán)內(nèi)部被調(diào)用了,所以color的next被調(diào)用完了。所以出現(xiàn)了NoSuchElementException。但是有時(shí)候也不會(huì)出現(xiàn)這種情況,場(chǎng)景是外部循環(huán)迭代器調(diào)用的次數(shù)剛好是內(nèi)部調(diào)用的n倍。
問題4:嵌套迭代不拋異常,但是結(jié)果不正確
這種情況是外部循環(huán)迭代器調(diào)用的次數(shù)剛好是內(nèi)部調(diào)用的n倍。我們?cè)賮?lái)個(gè)例子:
- public class Main {
- //枚舉顏色
- enum Color { RED, GREEN, BLUE, BLACK }
- public static void main(String[] args) {
- Collection<Color> colors = Arrays.asList(Color.values());
- //兩層for循環(huán)
- for (Iterator<Color> c1 = colors.iterator(); c1.hasNext(); ) {
- for (Iterator<Color> c2 = colors.iterator(); c2.hasNext(); ) {
- System.out.println(c1.next()+" "+c2.next());
- }
- }
- }
- }
現(xiàn)在對(duì)顏色進(jìn)行for循環(huán)遍歷,一共兩層for循環(huán),因?yàn)橐还灿兴姆N顏色,兩層for循環(huán)應(yīng)該是打印16個(gè)結(jié)果。現(xiàn)在運(yùn)行一遍看看結(jié)果:
- RED RED
- GREEN GREEN
- BLUE BLUE
- BLACK BLACK
沒錯(cuò),確實(shí)是打印了四條。原因和問題三是一樣的。有一種方式可以很好地解決這種嵌套的問題。
嵌套迭代問題解決:
直接看代碼。既然是外部的迭代器it在內(nèi)部使用了,那我在內(nèi)部和外部之間用一個(gè)變量緩存起來(lái)不就好了。
- public class Main {
- //枚舉顏色
- enum Color { RED, GREEN, BLUE, BLACK }
- public static void main(String[] args) {
- Collection<Color> colors = Arrays.asList(Color.values());
- //for循環(huán)
- for (Iterator<Color> c1 = colors.iterator(); c1.hasNext(); ) {
- //用一個(gè)變量緩存起來(lái)
- Color c = c1.next();
- for (Iterator<Color> c2 = colors.iterator(); c2.hasNext(); ) {
- System.out.println(c+" "+c2.next());
- }
- }
- }
- }
現(xiàn)在再來(lái)運(yùn)行,就可以很好地得出16種結(jié)果了。這種方式也比較不錯(cuò),但是卻不能很好地解決問題1和問題2。因此,為了解決這一現(xiàn)象,大佬Joshua Bloch在書中提出,推薦使用for-each循環(huán)來(lái)代替for循環(huán)。
二、for-each循環(huán)
既然作者推薦使用for-each循環(huán),我們看看他有什么好處。是如何解決上面的問題的。
- 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循環(huán)
- for (Color color:colors) {
- for (Size size:sizes ) {
- flowers.add(new Flower(color, size));
- }
- }
- }
- }
看里面的for-each循環(huán)。上面的問題就全都解決了。好吧,可能你會(huì)感覺,就這?還有一個(gè)好處還沒說,再往下看。
for-each 循環(huán)不僅允許遍歷集合和數(shù)組,還允許遍歷實(shí)現(xiàn) Iterable 接口的任何對(duì)象,該接口由單個(gè)方法組成。接 口定義如下:
- public interface Iterable<E> {
- // Returns an iterator over the elements in this iterable
- Iterator<E> iterator();
- }
如果必須從頭開始編寫自己的 Iterator 實(shí)現(xiàn),那么實(shí)現(xiàn) Iterable 會(huì)有點(diǎn)棘手,但是如果你正在編寫表示一組元素 的類型,那么你應(yīng)該強(qiáng)烈考慮讓它實(shí)現(xiàn) Iterable 接口,甚至可以選擇不讓它實(shí)現(xiàn) Collection 接口。這允許用戶使用for-each 循環(huán)遍歷類型,他們會(huì)永遠(yuǎn)感激不盡的 。
但是,有三種常見的情況是你不能分別使用 for-each 循環(huán)的:
(1)有損過濾(Destructive filtering):如果需要遍歷集合,并刪除指定選元素,則需要使用顯式迭代器,以便可以調(diào)用其 remove 方法。通常可以使用在 Java 8 中添加的 Collection 類中的 removeIf 方法,來(lái)避免顯式遍歷。
(2)轉(zhuǎn)換:如果需要遍歷一個(gè)列表或數(shù)組并替換其元素的部分或全部值,那么需要列表迭代器或數(shù)組索引來(lái)替換元素的值。
(3)并行迭代:如果需要并行地遍歷多個(gè)集合,那么需要顯式地控制迭代器或索引變量,以便所有迭代器或索引變量都可以同步進(jìn)行 。
如果發(fā)現(xiàn)自己處于這些情況中的任何一種,請(qǐng)使用傳統(tǒng)的 for 循環(huán),并警惕本條目中提到的陷阱 。
本文轉(zhuǎn)載自微信公眾號(hào)「愚公要移山」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系愚公要移山公眾號(hào)。