用 for/in 在 Java 5.0 中增強循環

轉自:http://www.ibm.com/developerworks/cn/java/j-forin.html 

2004 11 30

for/in 循環通常叫作 增強的 for 或者 foreach,它是 Java 5.0 中一個極爲方便的特性。實際上它沒有提供任何新的功能,但它顯然能讓一些日常編碼任務變得更簡單一些。在本文中,您將學習這方面的許多內容,其中包括使用 for/in 在數組和集合中進行遍歷,以及如何用它避免不必要(或者只是令人厭煩的)類型轉換。您還將學習如何實現 for/in,瞭解新的 Iterable 接口的一些細節,甚至還將學習如何讓您自己的定製對象用這個新的構造進行遍歷。最後,您將學習 for/in 不能 做什麼,以確保您能理解什麼時候選擇原來的 for 是正確的選擇。

越短越好

這是資深電腦程序員都知道的一條最基本的原理:因爲 更短 意味着 打字更少,所以更短自然也就更好。這個哲學造就了 vi 這樣的 IDE,在這類 IDE 中,像 :wq! 28G 這樣的命令擁有豐富的含義。這個哲學還導致一些最神祕的代碼,比如說,變量 ar 代表 Agile Runner(也可能是 Argyle,或者 Atomic Reactor 等等,總之,您明白就好)。

有些時候,在努力實現短小的時候,程序員會將明確性拋到腦後。也就是說,過於短小和過於繁冗的代碼都會讓人感到痛苦不堪。變量名爲 theAtomicReactorLocatedInPhiladelphia 與名爲 ar 的變量一樣讓人討厭和不方便。一定會有一個讓人高興的解決方法,不是嗎?

這個讓人高興的方法(至少我是這麼認爲的)是以尋找完成某事的 方便 途徑爲出發點,不是爲了短小而短小。作爲這類解決方案的一個好例子,Java 5.0 引入了新版的 for 循環,我把它稱爲 for/in。它也被稱爲 foreach,有時也叫作 增強的 for,但這些指的都是同一個構造。不管您叫它什麼, for/in 都會使代碼變得更簡單,正如您在本文中將看到的那樣。

不使用 Iterator

使用 for/in 普通”for 之間的最基本區別是,您不必使用計數器(通常稱爲 i count)或 Iterator。參見清單 1,它顯示了一個使用的 Iterator for 循環:


清單 1. for 循環,舊式學院風格

                           

       

public void testForLoop(PrintStream out) throws IOException {

  List list = getList();  // initialize this list elsewhere

 

  for (Iterator i = list.iterator(); i.hasNext(); ) {

    Object listElement = i.next();

    out.println(listElement.toString());

   

    // Do something else with this list element

  }

}

     

 

注意:如果您一直在看我寫的關於 Tiger 新特性的文章(請參閱 參考資料),您就會知道,我常常感謝 O'Reilly Media, Inc.,因爲它們允許我在本文中發佈我其他書中的代碼示例。這意味着您得到的代碼已經通過了更多測試、更多評論,比我能提供給您的多得多。所以再次感謝 O'Reilly,如果您想了解 Tiger 的更多內容,請參考我撰寫的一些書,它們列在 參考資源一節中,其中有完整的鏈接和更多的細節。

如果您期待着得到如何把這個代碼轉變成新的 for/in 循環的詳細解釋,我恐怕要讓您失望。清單 2 顯示了用 for/in 改寫的清單 1 中的代碼,您應該相當熟悉它。請參見下面代碼清單,我將儘可能詳細地解釋 for/in 循環(但是仍然很難湊成一章)。


清單 2. 轉換成 for/in

                           

       

public void testForInLoop(PrintStream out) throws IOException {

  List list = getList();  // initialize this list elsewhere

 

  for (Object listElement : list) {

    out.println(listElement.toString());

   

    // Do something else with this list element

  }

}

     

 

for/in 循環的基本語法如清單 3 所示。如果您還不習慣閱讀規範,那麼該語法可能看起來有點古怪,但是當您一個部分一個部分了解它的時候,您會發現閱讀它實際上非常容易。


清單 3. for/in 循環的基本結構

                           

       

for

        聲明:

        表達式)

 

        語句

                    

     

 

for/in 因何得名

細心的讀者會注意到,所謂 for/in 根據不包含單詞 in。它的名字來自借閱的閱讀方式。在清單 2 中,您會說 for 每個對象 in 命名變量列表中,執行 ... 。當然,省略號代表循環實質做的操作。您如何看待會有些差異,但是在每種表達方式中 for in 都是突出的。

聲明 是一個變量,例如 Object listElement。這個變量應該有自己的類型,這樣,它就可以與將遍歷的列表、數組或集合中的每一個項兼容。在清單 2 的例子中, list 包含一些對象,因此這些對象就是 listElement 的類型。

表達式 就是一個表達式。它計算的結果應當是可以遍歷的(後面再詳加介紹)。在現在,只要保證 表達式 計算的結果是一個集合或者數組就可以了。表達式可以簡單到就是一個變量(如清單 2 所示)或者是一個方法調用(例如 getList()),亦或是包含布爾邏輯或三目運算符的複雜表達式。只要它返回一個數組或集合,就一切 OK

語句 代表循環的內容,它對 聲明 中定義的變量進行操作;當然,這是一個循環,所以 語句 將應用到數組中集合的每個項目上。而且,使用大括號( { })時,還能使用多條語句。

其用法如下:創建一個變量,指向要遍歷的數組或集合,然後對定義的變量進行操作。不用對列表中的每個項目進行賦值,因爲 for/in 替您處理了這件事。當然,如果您還覺得不太清楚,沒關係,繼續讀下去,有大量的示例讓您足夠清楚這個事件。

但是,在進行下一步之前,我想用更加符合規範的方式說明 for/in 的工作方式。清單 4 顯示了在提供通用化類型時,實際發揮作用的 for/in 循環。以下是編譯器把該循環轉換成普通的 for 循環之後,語句實際看起來的樣子。

您明白了嗎?編譯器實際上把這個更短、更方便的 for/in 語句變成了一個更加編譯器友好的 for 循環,而且您不會受到這項工作的影響。這就是爲什麼我認爲它方便,而不僅僅說它更簡短的原因。


清單 4. 轉換後的 for/in 循環,帶有一個 Iterable

                           

       

for (Iterator<

        E> #i = (

        expression).iterator(); #i.hasNext(); ) {

 

        declaration = #i.next();

 

        statement

}

     

 

清單 5 是另外一個經過編譯器轉換之後的 for/in,這次沒有通用化類型。雖然更簡單,但做的事是一樣的。但是在每種情況下,您都可以很容易地在腦子裏(並通過編程方式)把 for/in 語句轉換成普通的 for 語句,如果您能在腦子子裏做這個轉換,事情就變得極爲容易了。


清單 5. 轉換後的 for/in 循環,沒有未經參數化的類型

                           

       

for (Iterator #i = (

        expression).iterator(); #i.hasNext(); ) {

 

        declaration = #i.next();

 

        statement

}

     

 

使用數組

現在您已經瞭解了基本的語義,可以繼續瞭解一些更具體的示例了。您已經看到 for/in 如何處理列表了;處理數組也一樣容易。與集合相同,數組也被賦值(如清單 6 所示),然後這些值被逐個取出,並被處理。


清單 6. 簡單的數組初始化

                           

       

int[] int_array = new int[4];

String[] args = new String[10];

float[] float_array = new float[20];

     

 

對於使用 for 以及計算器或索引變量的場合,現在就可以使用 for/in(當然,前提是您正在使用 Tiger)。清單 7 顯示了另外一個簡單的示例:


清單 7. for/in 對數組進行循環就是小菜一碟

                           

       

public void testArrayLooping(PrintStream out) throws IOException {

  int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 };

 

  // Print the primes out using a for/in loop

  for (int n : primes) {

    out.println(n);

  }

}

     

 

沒有任何需要特別說明的地方,這些都是非常基本的東西。數組被類型化,所以您需要很清楚地知道數組中每個項目的變量類型是什麼。這個示例創建了變量(在這個示例中名爲 n),然後對這個變量進行操作。非常簡單,不是嗎?我告訴過您在這裏沒有什麼複雜的東西。

實際上,數據中有什麼類型並不是問題,您只需爲 聲明 選擇好正確的類型就可以了。在清單 8 中,數組的元素是 Lists。所以您得到的實際上是一個集合數組。同樣,使用 for/in 就能使這些變得非常簡單。


清單 8. for/in 還可以在對象數組上循環

                           

       

public void testObjectArrayLooping(PrintStream out) throws IOException {

  List[] list_array = new List[3];

 

  list_array[0] = getList();

  list_array[1] = getList();

  list_array[2] = getList();

 

  for (List l : list_array) {

    out.println(l.getClass().getName());

  }

}

     

 

甚至還可以在 for/in 循環中再加上一層循環,如清單 9 所示:


清單 9. for/in 內部使用 for/in 不會有任何問題!

                           

       

public void testObjectArrayLooping(PrintStream out) throws IOException {

  List[] list_array = new List[3];

 

  list_array[0] = getList();

  list_array[1] = getList();

  list_array[2] = getList();

 

  for (List l : list_array) {

   

        for (Object o : l) {

      out.println(o);

    }

  }

}

     

 

處理集合

同樣,簡單性也是我們關注的內容。使用 for/in 對集合進行遍歷沒有任何需要特殊處理或者複雜的地方,它工作起來,與您剛纔看到的處理列表和集合的方式一樣。清單 10 演示了一個在 List Set 上遍歷的示例,毫無驚人之處。與往常一樣,我們將研究代碼,確保您瞭解發生的事情。


清單 10. 以下程序中有許多簡單循環,演示瞭如何使用 for/in

                           

       

package com.oreilly.tiger.ch07;

import java.util.ArrayList;

import java.util.HashSet;

import java.util.List;

import java.util.Set;

public class ForInDemo {

  public static void main(String[] args) {

 

    // These are collections to iterate over below

    List wordlist = new ArrayList();

    Set wordset = new HashSet();

   

    // Basic loop, iterating over the elements of an array

    //   The body of the loop is executed once for each element of args[].

    //   Each time through, one element is assigned to the variable word.

    System.out.println("Assigning arguments to lists...");

    for (String word : args) {

      System.out.print(word + " ");

      wordlist.add(word);

      wordset.add(word);

    }

   

    System.out.println();

   

    // Iterate through the elements of the List now

    //   Since lists have an order, these words should appear as above

    System.out.println("Printing words from wordlist " +

      "(ordered, with duplicates)...");

    for (Object word : wordlist) {

      System.out.print((String)word + " ");

    }

   

    System.out.println();

   

    // Do the same for the Set. The loop looks the same but by virtue

    //   of using a Set, word order is lost, and duplicates are discarded.

    System.out.println("Printing words from wordset " +

      "(unordered, no duplicates)...");

    for (Object word : wordset) {

      System.out.print((String)word + " ");

    }

  }

}

     

 

清單 11 顯示了這個程序的輸出(在命令行上輸出了一些用來演示的數據):


清單 11. 輸出正是您想要的 —— 許多打印!

                           

       

run-ch07:

    [echo] Running Chapter 7 examples from Java 5.0 Tiger: A Developer's Notebook

    [echo] Running ForInDemo...

    [java] Assigning arguments to lists...

    [java] word1 word2 word3 word4 word1

    [java] Printing words from wordList (ordered, with duplicates)...

    [java] word1 word2 word3 word4 word1

    [java] Printing words from wordset (unordered, no duplicates)...

    [java] word4 word1 word3 word2

     

 


 

類型轉換之痛

迄今爲止,在處理集合的時候,您已經看到 for/in 使用通用的變量類型,例如 Object。這麼做很好,但是沒有真正利用到 Tiger 的另一項特性 —— 泛型(有時也叫作 參數化類型)。我把泛型的細節留給 developerWorks 即將針對這個主題推出的教程,但是泛型讓 for/in 變得更加強大。

記得 for/in 語句的 聲明 部分創建了一個變量,它代表要遍歷的集合中每個項目的類型。在數組中,類型非常明確,因爲類型是強類型的, int[] 只能包含整數,所以在循環中沒有類型轉換。在您通過泛型使用類型化列表時,也有可能做到這點。清單 12 演示了幾個簡單的參數化集合:


清單 12. 向集合類型添加參數意味着可以避免以後的類型轉換

                           

       

List

        <String> wordlist = new ArrayList

        <String>();

Set

        <String> wordset = new HashSet

        <String>();

     

 

現在,您的 for/in 循環可以避開老式的 Object,變得更加具體。清單 13 演示了這一點:


清單 13. 在知道集合中的類型時,您的循環體可以更加具有類型針對性

                           

       

                           

        for (String word : wordlist) {

  System.out.print(word + " ");

}

     

 

作爲一個更加完整的示例,清單 14 沿用了清單 10 所示的程序,並添加了一些通用的列表和更加具體的 for/in 循環:


清單 14:可以利用泛型重寫清單 10

                           

       

package com.oreilly.tiger.ch07;

import java.util.ArrayList;

import java.util.HashSet;

import java.util.List;

import java.util.Set;

public class ForInDemo {

  public static void main(String[] args) {

 

    // These are collections to iterate over below

    

        List<String> wordlist = new ArrayList<String>();

    Set<String> wordset = new HashSet<String>();

   

    // Basic loop, iterating over the elements of an array

    //   The body of the loop is executed once for each element of args[].

    //   Each time through, one element is assigned to the variable word.

    System.out.println("Assigning arguments to lists...");

    for (String word : args) {

      System.out.print(word + " ");

      wordlist.add(word);

      wordset.add(word);

    }

   

    System.out.println();

   

    // Iterate through the elements of the List now

    //   Since lists have an order, these words should appear as above

    System.out.println("Printing words from wordlist " +

      "(ordered, with duplicates)...");

    for (

        String word : wordlist) {

      System.out.print((String)word + " ");

    }

   

    System.out.println();

   

    // Do the same for the Set. The loop looks the same but by virtue

    //   of using a Set, word order is lost, and duplicates are discarded.

    System.out.println("Printing words from wordset " +

      "(unordered, no duplicates)...");

    for (

        String word : wordset) {

      System.out.print((String)word + " ");

    }

  }

}

     

 

當然,在這些例子中,類型轉換還沒有完全消失。但是,這些工作正逐步轉交給編譯器完成(如果您對這類事情感興趣,那麼可以說這就是泛型或多或少要做的事)。在編譯的時候,所有這些類型都會被檢測,您可能得到相應的錯誤信息。如果有人能做這項工作,那麼,其他所有人也能這麼做,不是嗎?

Who the heck is E?

如果您是 Java 老手,但是剛接觸 Tiger,那麼所有對 E 的引用對您來說可能很奇怪。這些都是與參數化類型支持(泛型)有關,它允許 Iterator 可以處理類型化的集合 —— 例如, Iterator<String> 能處理這個新版本接口,敬請參閱 developerWorks 即將在 12 7 日推出的關於泛型的教程。

 


 

類與 for/in 的集成

迄今爲止,我只是針對 Java 事先打包的類和類型(arraylistmapset 和其他集合)進行遍歷。儘管這已經相當不錯,但編程語言的美麗在於它們能幫助您定義自己的類。定製對象是大型應用程序的支柱。這一節要處理的只是允許 for/in 構造使用您自己的對象所涉及的一些概念與步驟。

一個新接口

到了現在,您應當熟悉 java.util.Iterator 接口了,倘若您不熟悉它,清單 15 演示了這個接口,而且是按照它在 Tiger 出現的形式演示的:


清單 15. Iterator 長時間以來一直是 Java 語言的中流砥柱

                           

       

package java.util;

public interface Iterator<E> {

  public boolean hasNext();

 

  public E next();

 

  public void remove();

}

     

 

但是,爲了利用 for/in,需要在您的域知識中添加另一個接口 java.lang.Iterable。該接口如清單 16 所示:


清單 16. Iterable 接口是 for/in 構造的基礎

                           

       

package java.lang;

public interface Iterable<E> {

  public java.util.Iterator<E> iterator();

}

     

 

java.lang,而不是 java.util

請注意, Iterable 位於 java.lang 之中,而 不是位於 java.util 中。至於爲什麼會這樣,我沒有找到任何明確的文檔,但就我個人猜測,可能是爲了避免必須導入接口( java.lang 位於爲所有 Java 代碼自動導入的名稱空間集中)。

爲了讓您的對象或類能與 for/in 一起工作,對象和類需要實現 Iterable 接口。這留給您兩個基本場景:

擴展現有的、已經實現了 Iterable(因此也就已經支持 for/in)的集合類。

手動處理遍歷,定義自己的 Iterable 實現。

手動處理遍歷

如果有可能,我極力建議您用定製對象擴展現有的集合。事情會變得極爲簡單,而您可以避免所有繁瑣的細節。清單 17 顯示了一個這樣做的類:


清單 17. 擴展現有的集合是利用 for/in 的捷徑

                           

       

package com.oreilly.tiger.ch07;

import java.util.LinkedList;

import java.util.List;

public class GuitarManufacturerList extends LinkedList<String> {

  public GuitarManufacturerList() {

    super();

  }

  public boolean add(String manufacturer) {

    if (manufacturer.indexOf("Guitars") == -1) {

      return false;

    } else {

      super.add(manufacturer);

      return true;

    }

  }

} 

     

 

因爲 LinkedList 已經可以使用 for/in,所以,不需要特殊的代碼,就可以在 for/in 中使用這個新類。清單 18 演示了這點,以及做到這一點需要做的工作是多麼地少:


清單 18. Iterable 接口是 for/in 構造的基礎

                           

       

package com.oreilly.tiger.ch07;

import java.io.IOException;

import java.io.PrintStream;

public class CustomObjectTester {

 

        /** A custom object that extends List */

  private GuitarManufacturerList manufacturers;

 

  public CustomObjectTester() {

   

        this.manufacturers = new GuitarManufacturerList<String>();

  }

 

  public void testListExtension(PrintStream out) throws IOException {

    // Add some items for good measure

    manufacturers.add("Epiphone Guitars");

    manufacturers.add("Gibson Guitars");

   

    // Iterate with for/in

   

        for (String manufacturer : manufacturers) {

      out.println(manufacturer);

    }

  }

 

  public static void main(String[] args) {

    try {

      CustomObjectTester tester = new CustomObjectTester();

     

      tester.testListExtension(System.out);

    } catch (Exception e) {

      e.printStackTrace();

    }

  }

}

     

 

手動處理遍歷

在某些不常見的情況下 —— 老實說,我費了很大勁想到了很多 —— 在您的定製對象可以遍歷的時候,您可能需要執行特定的行爲。在這些(相當不幸)的情況下,您必須自己處理這些事情。清單 19 演示瞭如何做,雖然需要做很多工作,但是並不複雜,所以我把代碼留給您自己來看。以下這個類提供了文本文件的包裝器,在遍歷它的時候,它將列出文件中的每行內容。


清單 19. 耐心點,您自己也能實現 Iterable 接口,並在循環中提供定製行爲

                           

       

package com.oreilly.tiger.ch07;

import java.util.Iterator;

import java.io.BufferedReader;

import java.io.FileReader;

import java.io.IOException;

/**

 * This class allows line-by-line iteration through a text file.

 *   The iterator's remove() method throws UnsupportedOperatorException.

 *   The iterator wraps and rethrows IOExceptions as IllegalArgumentExceptions.

 */

public class TextFile implements Iterable<String> {

  // Used by the TextFileIterator below

  final String filename;

 

  public TextFile(String filename) {

    this.filename = filename;

  }

 

  // This is the one method of the Iterable interface

  public Iterator<String> iterator() {

    return new TextFileIterator();

  }

 

  // This non-static member class is the iterator implementation

  class TextFileIterator implements Iterator<String> {

 

    // The stream being read from

    BufferedReader in;

   

    // Return value of next call to next()

    String nextline;

   

    public TextFileIterator() {

      // Open the file and read and remember the first line

      //   Peek ahead like this for the benefit of hasNext()

      try {

        in = new BufferedReader(new FileReader(filename));

        nextline = in.readLine();

      } catch (IOException e) {

        throw new IllegalArgumentException(e);

      }

    }

   

    // If the next line is non-null, then we have a next line

    public boolean hasNext() {

      return nextline != null;

    }

   

    // Return the next line, but first read the line that follows it

    public String next() {

      try {

        String result = nextline;

       

        // If we haven't reached EOF yet...

        if (nextline != null) {

          nextline = in.readLine();    // Read another line

          if (nextline == null)

            in.close();                // And close on EOF

        }

       

        // Return the line we read last time through

        return result;

       

      } catch (IOException e) {

        throw new IllegalArgumentException(e);

      }

    }

   

    // The file is read-only; we don't allow lines to be removed

    public void remove() {

      throw new UnsupportedOperationException();

    }

  }

 

  public static void main(String[] args) {

    String filename = "TextFile.java";

    if (args.length > 0)

      filename = args[0];

     

    for (String line : new TextFile(filename))

      System.out.println(line);

  }

}

     

 

其中大部分工作是實現 Iterator,然後通過 iterator() 方法返回它。其他的事情就非常簡單了。但是,您可以看到,與擴展一個現成的類來完成同樣的工作相比,手動實現 Iterable 接口需要做的工作多得多。

 


 

不能做什麼

我確實認爲 for/in 是這些好東西中的一個,但是與所有的好東西一樣,它們也有自身的侷限性。原因是 for/in 設置的方式,特別是因爲它沒有顯式地使用 Iterator,所以使用這個新構造時,有些事情是您不能做的。

定位

最明顯的顯然是不能確定您在列表或數組(或者定製對象)中的位置。爲了提醒您,清單20 顯示了典型 for 循環的一個可能用法。請注意,索引變量不僅能是在列表中移動,還能指示其所在位置:


清單 20. 在普通的循環中使用迭代變量

                           

       

List<String> wordList = new LinkedList<String>();

for (int i=0; i<args.length; i++) {

  wordList.add("word " + (i+1) + ": '" + args[i] + "'");

}

     

 

這不是什麼古怪的用法,而是很普通的編程方式。但是,您不能用 for/in 完成這個簡單的任務,如清單 21 所示:


清單 21. 不可能在 for/in 循環中訪問位置

                           

       

public void determineListPosition(PrintStream out, String[] args)

    throws IOException {

   

    List<String> wordList = new LinkedList<String>();

   

   

        // Here, it's easy to find position

    for (int i=0; i<args.length; i++) {

      wordList.add("word " + (i+1) + ": '" + args[i] + "'");

    }

   

   

        // Here, it's

          not possible to locate position

       

    for (String word : wordList) {

      out.println(word);

    }

}

     

 

在這裏,沒有任何類型的計數器變量(或者 Iterator),也不存在任何僥倖。如果需要定位,就得用普通 for。清單 22 顯示了定位的另外一個常見用法 —— 處理字符串:


清單 22. 另一個問題 —— 字符串連接

                           

       

StringBuffer longList = new StringBuffer();

for (int i=0, len=wordList.size(); i < len; i++) {

  if (i < (len-1)) {

    longList.append(wordList.get(i))

            .append(", ");

  } else {

    longList.append(wordList.get(i));

  }

}

out.println(longList);

     

 

刪除項目

另外一個限制是項目刪除。如清單 23 所示,在列表遍歷期間無法刪除項目:


清單 23. for/in 循環中無法刪除項目

                           

       

public void removeListItems(PrintStream out, String[] args)

    throws IOException {

   

    List<String> wordList = new LinkedList<String>();

   

    // Assign some words

    for (int i=0; i<args.length; i++) {

      wordList.add("word " + (i+1) + ": " '" + args[i] + "'");

    }

   

    // Remove all words with "1" in them.

        Impossible with for/in!

    for (Iterator i = wordList.iterator(); i.hasNext(); ) {

      String word = (String)i.next();

      if (word.indexOf("1") != -1) {

       

        i.remove();

      }

    }

   

    // You can print the words using for/in

    for (String word : wordList) {

      out.println(word);

    }

}

     

 

從整體來看,這些不算什麼限制,只是什麼時候使用 for、什麼時候使用 for/in 的一個準則。可能是一些不值一提的細節。

最糟糕的結果是您可能找不到 需要 for/in 的地方,這也正是我所擔心的。請記住,for/in 是一項很方便的功能,它能讓代碼更清晰、更簡潔,同時也能讓代碼簡潔得讓人頭痛。

 

 

發佈了33 篇原創文章 · 獲贊 7 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章