Java 中的迭代器 —— Iterator

一、 Iterator 是什麼?

1、迭代器模式

迭代器模式(Iterator Pattern)是一種非常常見的設計模式,這種模式用於順序訪問集合對象的元素,而不需要知道集合對象內部的實現方式。

所以,迭代器模式的優點就是:簡化了聚合類。無論是增加新的聚合類還是增加迭代器類都會很方便,無須修改原有的代碼。

它的優點也導致了它的缺點:由於迭代器模式將存儲數據和遍歷數據的職責分離,增加新的聚合類時也需要對應增加新的迭代器類,耦合度很高,這在一定程度上增加了系統的複雜性。

2、Iterator 接口

Java 中,提供了一個迭代器接口 Iterator ,把在集合對象中元素之間遍歷的工作交給迭代器,而不是集合對象本身,迭代器爲遍歷不同的集合對象提供一個統一的接口。這就是 Java 集合框架中 Iterable 接口位於框架結構最頂層的原因。這其實也就是面向對象的思想。

二、Iterator 的使用

下面我們先看看 Iterator 是如何使用的。

1、Iterator 中的方法

先從 Iterator 接口的源碼來分析一下:

public interface Iterator<E> {
    boolean hasNext();

    E next();

    default void remove() {
      throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
      Objects.requireNonNull(action);
      while (hasNext())
        action.accept(next());
    }
  }

可以看到,Iterator 接口提供的方法很少,也比較簡單:

  • boolean hasNext():如果迭代中還有更多的元素,則返回 true,否則返回 false
  • E next():返回迭代中的下一個元素;
  • default void remove():從集合中移除此迭代器返回的最後一個元素,它是一個可選的操作;
  • default void forEachRemaining(Consumer action) :對每個剩餘元素執行給定的操作,直到所有元素都已處理完畢或該操作引發異常。

2、Iterator 基本示例

瞭解了 Iterator 提供的方法,下面我們來使用一下:

import java.util.Iterator;
import java.util.LinkedList;

public class Main {
  public static void main(String[] args) {
    LinkedList<String> names = new LinkedList<String>();
    names.add("Deepspace");
    names.add("chenxingxing");

    Iterator<String> namesIterator = names.iterator();

    while (namesIterator.hasNext()) {
      System.out.println(namesIterator.next());
    }
  }
}

輸出結果爲:

Deepspace
chenxingxing

其實,我們也可以使用 foreach 方法來遍歷集合:

for (String name : names) {
  System.out.println(name);
}

我們再試試 remove 方法:

public class Main {
  public static void main(String[] args) {

    List<String> names = new LinkedList<String>();
    names.add("E-1");
    names.add("E-2");
    names.add("E-3");
    names.add("E-n");

    Iterator<String> namesIterator = names.iterator();

    while (namesIterator.hasNext()) {
      String next = namesIterator.next();
      System.out.println(next);

      if ("E-3".equals(next)) {
        namesIterator.remove();
      }
    }
    System.out.println(names); // [E-1, E-2, E-n]
  }
}

可以看到 E-3 這個元素被移除掉了。

再看看 forEachRemaining 方法(注意,該方法是沒有返回值的):

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class Main {
  public static void main(String[] args) {

    List<String> names = new LinkedList<String>();
    names.add("E-1");
    names.add("E-2");
    names.add("E-3");
    names.add("E-n");

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

    Iterator<String> namesIterator = names.iterator();

    while (namesIterator.hasNext()) {
      String next = namesIterator.next();
      if ("E-3".equals(next)) {
        namesIterator.remove();
        namesIterator.forEachRemaining(targetList::add); // 這裏用到了 Java8 裏的新語法
      }
    }
    System.out.println(targetList); // [E-3, E-n]
  }
}

三、Iterator 內部是如何工作的?

下面我們來了解下 Java 迭代器及其方法是如何在內部工作的。以 LinkedList 對象爲例。

創建一個 List 集合,並插入幾條數據:

List<String> names = new LinkedList<String>();
names.add("E-1");
names.add("E-2");
names.add("E-3");
names.add("E-n");

現在在 List 對象上創建一個迭代器對象:

Iterator<String> namesIterator = names.iterator();

可以用下面的圖來表示 nameIterator

迭代器1

這裏,IteratorCursor (光標)指向 List 的第一個元素之前。

下面我們再運行下面兩行代碼:

namesIterator.hasNext();
namesIterator.next();

這個時候,IteratorCursor 指向 List 中的第一個元素,如下圖所示:

迭代器2

我們再運行一下剛纔的兩行代碼:

namesIterator.hasNext();
namesIterator.next();

IteratorCursor 會指向 List 中的第二個元素:

迭代器3

以此類推,重複執行此過程,可將 IteratorCursor 指向 List 中的最後一個元素。

迭代器4

當讀取最後一個元素後,如果繼續運行下面的代碼片段,它將返回 false

迭代器5

從上面的描述可以看出,Java 迭代器只支持如下圖所示的前進方向迭代

迭代器6

所以在一些地方迭代器也稱爲單向光標。

四、Iterator 的優缺點

從前面的講解中,我們可以知道 Iterator 有以下優點:

  • 可以在任何集合類中使用它,對於集合 API 是通用的;
  • 它支持 READREMOVE 操作;
  • 它的方法名稱簡單且易於使用。

但是它也有一些缺陷和限制:

  • CRUD 操作中,它不支持 CREATEUPDATE 操作;
  • 它只支持連續迭代(正向迭代),迭代時不支持迭代元素並行(與 Spliterator 相比,我們待會講到)。

什麼是並行呢?

並行指的是以某種方式利用多核 CPU 單元進行編程。也就是說,要完成一項任務,可以將其分解爲可以並行處理的單獨的子任務,然後彙總所有已處理單元的結果以完成原始工作

五、ListIterator

Java 中也提供了一個 ListIterator 接口,該接口繼承了 Iterator 接口,只對 List 實現的類有用。我們看看源碼:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int nextIndex();

    int previousIndex();

    void remove();

    void set(E e);

    void add(E e);
  }

可以看到,ListIterator 接口新增了一些方法,可以在迭代的時候進行 UPDATEADD 的操作,所以 ListIterator 接口就完全支持 CURD 操作了。

同時,我們也可以從 newIndexprevious 方法中發現,ListIterator 是一個雙向迭代器,支持正向迭代和反向迭代。

這裏要注意ListIterator 沒有當前元素;它的光標位置始終位於 previous() 返回的元素和 next() 的返回的元素之間。

1、ListIterator 基本示例

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class Main {
  public static void main(String[] args) {

    List<String> names = new LinkedList<String>();
    names.add("E-1");
    names.add("E-2");
    names.add("E-3");
    names.add("E-n");

    ListIterator<String> namesIterator = names.listIterator();

    while (namesIterator.hasNext()) {
      String next = namesIterator.next();
      System.out.println(next);
      if ("E-3".equals(next)) {
        namesIterator.add("E-4");
      }
      if ("E-n".equals(next)) {
        namesIterator.set("E-5");
      }
    }

    System.out.println("\nFor Loop");
    for (String name : names) {
      System.out.println(name);
    }
  }
}

輸出結果爲:

E-1
E-2
E-3
E-n

For Loop
E-1
E-2
E-3
E-4
E-5

上面的代碼演示了 UPDATEADD 操作。下面我們再看看,ListIterator 的方法如何執行前向和後向迭代。

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class Main {
  public static void main(String[] args) {

    List<String> names = new LinkedList<String>();
    names.add("E-1");
    names.add("E-2");
    names.add("E-3");
    names.add("E-n");

    ListIterator<String> namesIterator = names.listIterator();

    System.out.println("Forward Direction Iteration:");
    while(namesIterator.hasNext()){
      System.out.println(namesIterator.next());
    }

    System.out.println("Backward Direction Iteration:");
    while(namesIterator.hasPrevious()){
      System.out.println(namesIterator.previous());
    }
  }
}

輸出結果爲:

Forward Direction Iteration:
E-1
E-2
E-3
E-n
Backward Direction Iteration:
E-n
E-3
E-2
E-1

2、ListIterator 的侷限性

Iterator 相比,ListIterator 有許多優勢。 但是它仍然存在一些侷限性:

  • 它不適用於整個集合 API,只對 List 實現的類有用,而 Iterator 支持所有的集合類型;
  • 依然不支持元素的並行迭代;

六、Spliterator

最早的時候,那個時候 CPU 還是單核時代,Java 提供順序遍歷迭代器 Iterator 時,可以滿足需求;但到了多核時代下,順序遍歷已經不能滿足需求了,如何把多個任務分配到不同核上並行執行,最大發揮多核的能力,所以 Spliterator 就誕生了。

SpliteratorJava8 中新增的一個 API,除了支持順序遍歷之外,還支持高效的並行遍歷。

由於是 Java8 中新增的 API,所以在介紹它的使用上,讀者需要具備 StreamLambda 表達式的一些知識。

1、Spliterator 中的方法

1).characteristics():返回 Spliterator 數據對應的特徵值。根據文檔給出的例子,它返回的是多個特徵碼的按位或的結果:

public int characteristics() {
  return ORDERED | SIZED | IMMUTABLE | SUBSIZED;
}

2).hasCharacteristics(int characteristics):如果這個 Spliterator.characteristics() 包含所有給定的特徵,則返回 true

3).estimateSize():在執行前,將要遍歷遇到的元素數量進行估算並返回估計值(long 類型),如果無法返回(包括正無窮、位置或者計算超時等情況)則返回 long.MAX_VALUE

4).forEachRemaining(E e):在當前線程中,按順序對集合中的每個剩餘元素執行給定操作;

5).getComparator():如果該 Spliterator 的源是由 Comparator 排序的,則會將 Comparator 返回;

6).getExactSizeIfKnown():如果 estimateSize 的大小已知,則返回 .estimateSize() 方法的返回值,也就是數量的大小,否則返回 -1

7).tryAdvance(E e):如果存在剩餘元素,則對其執行給定操作,成功則返回 true,否則返回 false

  • 注意,該方法和 forEachRemaining 是有區別的,官方給的示例是:

    public boolean tryAdvance(Consumer<? super T> action) {
      if (origin < fence) {
        action.accept((T) array[origin]);
        origin += 2;
        return true;
      } else // cannot advance
        return false;
    }
    
  • 從中我們可以看到該方法只執行一次,如果成功就返回 true 否則返回 false,而 forEachRemaining 是會循環執行的:

    public void forEachRemaining(Consumer<? super T> action) {
      for (; origin < fence; origin += 2)
        action.accept((T) array[origin]);
    }
    

8).trySplit():這個方法其實就是負責並行的方法,官方文檔是這樣描述的:

 * <p>A Spliterator may also partition off some of its elements (using
 * {@link #trySplit}) as another Spliterator, to be used in
 * possibly-parallel operations.  Operations using a Spliterator that
 * cannot split, or does so in a highly imbalanced or inefficient
 * manner, are unlikely to benefit from parallelism.  Traversal
 * and splitting exhaust elements; each Spliterator is useful for only a single
 * bulk computation.

理解起來就是,可以使用這個方法對 Spliterator 對象的元素進行切分,用切分出來的部分創建一個新的Spliteraor 對象並返回,以方便進行並行操作。而調用該方法的線程會將返回的 Spliterator 交給另一個新的線程,新的線程又可以繼續分區,這樣就使得程序的執行速度大大提高。

那該分成多少個線程呢?如果線程池中線程數量過多,最終它們會與處理其它任務的線程來競爭稀缺的處理器和內存資源,浪費大量的時間在上下文切換上。反之,如果線程的數目過少,那麼多核處理器的一些核可能就無法充分利用。這個問題在這裏暫不討論,會在併發編程裏講解。

2、Spliterator 的基本示例

下面我們將通過一個例子討論 Spliterator 如何使用並行化更有效地遍歷我們可以分解的 Stream

先看看其中的一些方法:

import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
import java.util.stream.Stream;

public class Main {
  public static void main(String[] args) {
    List<String> mutants = new ArrayList<>();
    mutants.add("Professor X");
    mutants.add("Magneto");
    mutants.add("Storm");
    mutants.add("Jean Grey");
    mutants.add("Wolverine");
    mutants.add("Mystique");

    // Obtain a Stream to the mutants List.
    Stream<String> mutantStream = mutants.stream();

    // Getting Spliterator object on mutantStream.
    Spliterator<String> mutantList = mutantStream.spliterator();

    // .estimateSize() method
    System.out.println("Estimate size: " + mutantList.estimateSize());

    // .getExactSizeIfKnown() method
    System.out.println("\nExact size: " + mutantList.getExactSizeIfKnown());

    System.out.println("\nContent of List:");
    // .forEachRemaining() method
    mutantList.forEachRemaining((n) -> System.out.println(n));
  }
}

輸出結果爲:

Estimate size: 6

Exact size: 6

Content of List:
Professor X
Magneto
Storm
Jean Grey
Wolverine
Mystique

3、實現並行

再看看使用 trySplit() 方法,實現並行:

import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
import java.util.stream.Stream;

public class Main {
  public static void main(String[] args) {
    List<String> mutants = new ArrayList<>();
    mutants.add("Professor X");
    mutants.add("Magneto");
    mutants.add("Storm");
    mutants.add("Jean Grey");
    mutants.add("Wolverine");
    mutants.add("Mystique");

    Stream<String> mutantStream = mutants.stream();
    // 創建一個新的 Stream.
    Spliterator<String> splitList1 = mutantStream.spliterator();
    // 調用 .trySplit() 方法,從 splitList1 中拆分一個 splitList2
    Spliterator<String> splitList2 = splitList1.trySplit();
    // 如果 splitList1 可以被拆分,也就說 splitList2 不爲 null, 那就使用 splitList2.
    if (splitList2 != null) {
      System.out.println("\nOutput from splitList2:");
      splitList2.forEachRemaining((n) -> System.out.println(n));
    }
    // 這裏用 splitList1
    System.out.println("\nOutput from splitList1:");
    splitList1.forEachRemaining((n) -> System.out.println(n));
  }
}

輸出結果爲:

Output from splitList2:
Professor X
Magneto
Storm

Output from splitList1:
Jean Grey
Wolverine
Mystique

七、Iterable 接口

我們發現,在集合框架的最頂層就是 Iterable 接口,該接口中只有三個方法,源碼如下:

public interface Iterable<T> {
  Iterator<T> iterator();

  default void forEach(Consumer<? super T> var1) {
    Objects.requireNonNull(var1);
    Iterator var2 = this.iterator();

    while(var2.hasNext()) {
      Object var3 = var2.next();
      var1.accept(var3);
    }

  }

  default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(this.iterator(), 0);
  }
}

其中,返回了一個 IteratorSpliterator 方法是 Java8 中新增的。

那麼問題就來了,爲什麼集合框架一定要實現 Iterable 接口,而不直接實現 Iterator 接口呢?

通過前面的講解,我們知道 Iterator 接口的核心方法是 next()hasNext() 方法,而這兩個方法是依賴於迭代器的當前迭代位置的。如果 Collection 直接實現 Iterator 接口,那就會導致集合對象中包含當前迭代位置的數據(指針)。

當集合在不同方法間被傳遞時,由於當前迭代位置不可預知,那麼 next() 方法的結果也就會變成不可預知。 除非再爲 Iterator 接口添加一個 reset() 方法,用來重置當前迭代位置。 但是即使這樣做的話,Collection 也只能同時存在一個當前迭代位置

而集合框架實現了 Iterable 接口,每次調用都會返回一個從頭開始計數的迭代器,這樣多個迭代器是互不干擾的。

所以,基於上面的原因,集合框架實現的是 Iterable 接口,而不是直接實現 Iterator 接口。

八、編寫自定義的 Iterator

有的時候,我們需要要創建一個自定義的 Iterator 接口。

通過 Iterator 接口的源碼,我們知道:要創建自定義 Iterator,我們最少需要實現 .hasNext().next() 方法。

CustomList.java

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

public class CustomList<T> implements Iterable<T> {
  private List<T> list;

  CustomList(List<T> list) {
    this.list = list;
  }

  public Iterator<T> iterator() {
    return new EvenIterator<T>();
  }

  private class EvenIterator<T> implements Iterator<T> {
    int size = list.size();
    int currentPointer = 0;

    public boolean hasNext() {
      return (currentPointer < size);
    }

    public T next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      T val = (T) list.get(currentPointer);
      currentPointer += 1;
      return val;
    }
  }
}

Student.java

public class Student {
  private String name;
  private int age;

  public Student(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @Override
  public String toString() {
    return "name: " + name + ", age: " + age + ".";
  }
}

Main.java

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Main {
  public static void main(String[] args) {
    List<Student> list = new ArrayList<>();
    list.add(new Student("xiaoming", 22));
    list.add(new Student("xiaohgong", 33));
    list.add(new Student("xiaobai", 34));
    list.add(new Student("xiaohuang", 48));

    CustomList<Student> myList = new CustomList<Student>(list);

    Iterator<Student> iter = myList.iterator();
    while (iter.hasNext()) {
      System.out.println(iter.next());
    }
  }
}

輸出結果爲:

name: xiaoming, age: 22.
name: xiaohgong, age: 33.
name: xiaobai, age: 34.
name: xiaohuang, age: 48.

九、Enumeration 接口

Enumeration 接口也可以用於迭代,其中定義了一些方法,通過這些方法可以枚舉(一次獲得一個)對象集合中的元素。

public interface Enumeration<E> {
  boolean hasMoreElements();
  E nextElement();
}
  • hasMoreElements() 方法用於檢測 Enumeration 對象中是否還有元素,有則返回 true,否則返回 false
  • nextElement(): 如果 Enumeration 對象還有元素,該方法用於獲取下一個元素。

可以看到,Enumeration 接口的作用與 Iterator 接口類似。但是它只提供了遍歷 VectorHashtable 類型集合元素的枚舉功能,不支持元素的移除操作

看個例子:

import java.util.Enumeration;
import java.util.Vector;

public class Main {
  public static void main(String[] args) {
    Enumeration<Integer> company;
    Vector<Integer> employees = new Vector<Integer>();

    // add values to employees
    employees.add(1001);
    employees.add(2001);
    employees.add(3001);
    employees.add(4001);
    
    company = employees.elements();

    while (company.hasMoreElements()) {
      // get elements using nextElement() 
      System.out.println("Emp ID = " + company.nextElement());
    }
  }
}

1、Enumeration 接口和 Iterator 接口的區別

下面我們看看兩者的區別:

1)函數接口不同

  • Enumeration 只有 2 個函數接口;通過 Enumeration,我們只能讀取集合的數據,而不能對數據進行修改;
  • Iterator 接口中還有其他方法,除了能讀取集合的數據之外,也能對數據進行刪除操作。

2)Iterator 支持 fail-fast 機制,而 Enumeration 不支持

什麼是 fail-fast 機制呢?

fail-fast 機制是 Java 集合中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生 fail-fast 事件。例如:當某一個線程 A 通過 Iterator 去遍歷某集合的過程中,若該集合的內容被其他線程所改變了,那麼線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。

EnumerationJDK 1.0 添加的接口。使用到它的函數包括 VectorHashtable 等類,這些類都是 JDK 1.0 中加入的,Enumeration 存在的目的就是爲它們提供遍歷接口。Enumeration 本身並沒有支持同步,而在 VectorHashtable 實現 Enumeration 時,添加了同步。

IteratorJDK 1.2 才添加的接口,它也是爲了 HashMapArrayList 等集合提供遍歷接口。 Iterator 是支持 fail-fast 機制的,當多個線程對同一個集合的內容進行操作時,就可能會產生 fail-fast 事件。

所以,使用 Iterator 更加安全。

3)性能方面,對於 VectorHashtable 類型集合來說,Enumeration 的速度比 Iterator 接口快,同時所需的內存也遠比 Iterator 低。

我們測試一下:

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Random;

public class IteratorEnumeration {
  public static void main(String[] args) {
    int val;
    Random r = new Random();
    Hashtable table = new Hashtable();
    for (int i = 0; i < 1000000; i++) {
      // 隨機獲取一個[0,100)之間的數字
      val = r.nextInt(100);
      table.put(String.valueOf(i), val);
    }
    // 通過Iterator遍歷Hashtable
    iterateHashtable(table);
    // 通過Enumeration遍歷Hashtable
    enumHashtable(table);
  }

  /*
   * 通過Iterator遍歷Hashtable
   */
  private static void iterateHashtable(Hashtable table) {
    long startTime = System.currentTimeMillis();
    Iterator iter = table.entrySet().iterator();
    while (iter.hasNext()) {
      iter.next();
    }
    long endTime = System.currentTimeMillis();
    countTime(startTime, endTime);
  }

  /*
   * 通過Enumeration遍歷Hashtable
   */
  private static void enumHashtable(Hashtable table) {
    long startTime = System.currentTimeMillis();
    Enumeration enu = table.elements();
    while (enu.hasMoreElements()) {
      enu.nextElement();
    }
    long endTime = System.currentTimeMillis();
    countTime(startTime, endTime);
  }

  private static void countTime(long start, long end) {
    System.out.println("time: " + (end - start) + "ms");
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章