Iterator

可在 容器 上遍访的接口,调用人员无需关心容器的实现细节。

for 和 Iterator

我们可以用下面这样的 for 循环语句来遍历数组 arr 中的元素。

1
2
3
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

for 循环中的 i++ 的作用是让 i 的值在每次循环之后自增 1,这样就可以访问数组中的下一个元素,下下一个元素,下下下一个元素,从而实现了从头至尾遍历数组元素的功能。

将上面循环变量 i 的作用抽象化之后形成的模式,称为 Iterator 模式。下面让我们来看一段实现了 Iterator 模式的示例程序。这段程序的作用是将 Book 放置到 BookShelf 中,并将 Book 的名字按顺序打印出来。

示例程序

类图

iterator 的类图

Aggregate

Aggregate 接口中只声明了一个 iterator 方法,该方法会生成一个用来遍历集合的迭代器。

1
2
3
4
public interface Aggregate {
public abstract Iterator iterator();
}

Iterator

Iterator 接口用于遍历集合中的元素,其作用相当于循环语句中的循环变量。

1
2
3
4
5
public interface Iterator {
public abstract boolean hasNext();
public abstract Object next();
}

在这里我们声明了两个方法,即:

  • hasNext 方法判断下一个元素是否存在。
    • 该方法返回值是 boolean 型的。其原因很简单,当集合中存在下一个元素时,返回 true;当集合中不存在下一个元素时,返回 false。hasNext 主要用作循环终止条件。
  • next 方法获取下一个元素。
    • 该方法的返回值类型是 Object,表示该方法能返回任意类型的元素。但 next 方法的作用却不止如此。为了能在下次调用 next 方法时正确地返回下一个元素,该方法中还隐含着将迭代器移动至下一个元素的处理逻辑。

Book

Book 是表示书的类。这个类的作用有限,只有通过 getName 方法获取书名这一个作用。

1
2
3
4
5
6
7
8
9
10
11
public class Book {
private String name;

public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

BookShelf

BookShelf 是表示书架的类。由于需要将该类作为集合处理,所以它实现了 Aggregate 接口。因此,它 @Override 接口中的 iterator 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BookShelf implements Aggregate {
private Book[] books;
private int last = 0;

public BookShelf(int initialSize) {
this.books = new Book[initialSize];
}
public Book getBookAt(int index) {
return (Book) books[index];
}
public void appendBook(Book book) {
books[last] = book;
last++;
}
public int getLength() {
return last;
}
@Override
public Iterator iterator() {
return new BookShelfIterator(this);
}
}

  • 这个书架中定义了 books 字段,它是 Book 类型的数组。该数组的大小(initialSize)在生成 BookShelf 实例的时候就被指定了。
  • 之所以将 books 字段的可见性设置为 private,是为了防止外部不小心改变了该字段的值。

BookShelfIterator

因为 BookShelfIterator 类需要发挥 Iterator 的作用,所以它实现了 Iterator 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BookShelfIterator implements Iterator {
private BookShelf bookShelf;
private int index;

public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}
@Override
public Object next() {
Book book = bookShelf.getBookAt(index);
index++;
return book;
}
}

  • bookShelf 字段表示迭代器将要遍历的书架。index 字段表示迭代器当前所指向的书的下标。
  • 构造函数会将传入的 BookShelf 实例保存在 bookShelf 字段中,并将 index 初始化为 0。
  • hasNext 方法是 Iterator 接口中声明的方法。该方法会判断书架中还有没有下一本书,如果有就返回 true,如果没有就返回 false。而要知道书架中有没有下一本书,可以通过比较 index 和书架中书的总册数(bookShelf.getLength() 的返回值)来判断。
  • next 方法会返回迭代器当前所指向的书(Book 的实例),并让迭代器指向下一本书。如果与前面的 for 循环相比,这里的 index++ 的相当于其中的 i++,它让循环变量指向下一个元素。

Main

至此,遍历书架的准备工作就完成了。接下来我们利用 Main 类来制作一个小书架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(4);
bookShelf.appendBook(new Book("Around the World in 80 Days"));
bookShelf.appendBook(new Book("Bible"));
bookShelf.appendBook(new Book("Cinderella"));
bookShelf.appendBook(new Book("Daddy Long Legs"));

Iterator it = new BookShelfIterator(bookShelf);
while (it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}

}
}

这段程序首先设计了一个能容纳 4 本书的书架,然后按照书名顺序一次向书架中放入了下面这 4 本书。

  • Around the World in 80 Days
  • Bible
  • Cinderella
  • Daddy Long Legs

通过 bookShelf.iterator() 得到了遍历书架的 it 实例。while 的条件自然就是 it.hasNext()。只要书架上有书,while 循环就不会终止。然后,程序会通过 it.next() 一本一本地遍历书架中的书。

运行结果

总结

不管实现(BookShelf)如何变化,都可以使用 Iterator

为什么要引入 Iterator 这种复杂的设计模式呢?如果是数组,直接 for 循环语句进行遍历不就行了吗?为什么要在集合之外引入 Iterator 这个角色呢?一个重要的理由是,引入 Iterator 后可以将遍历与实现分离开来。请看如下代码。

1
2
3
4
while(it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}
  • 这里只使用了 Iterator 的 hasNext 方法和 next 方法,并没有调用 BookShelf 的方法。也就是说,这里的 while 循环并不依赖与 BookShelf 的实现。如果编写 BookShelf 的开发人员决定放弃用数组来管理书本,而是用 java.util.ArrayList 取而代之,会怎样呢?
  • 其实不管 BookShelf 如何变化,只要 BookShelf 的 iterator 方法能正确地返回 Iterator 的实例(换句话说,hasNext 和 next 方法都可以正常工作),即使不对上面的 while 循环做任何修改,代码都可以正常工作。这对于 BookShelf 的调用者来说真是太方便了。

设计模式的作用就是帮助我们编写可复用的类。 所谓『可复用』就是指将类实现为『组件』。当一个组件发生改变时,不需要对其它的组件进行修改或是只需要很小的修改即可应对。

容易弄错『下一个』

在 Iterator 模式的实现中,很容易在 next 方法上出错。该方法的返回值到底是应该指向当前元素还是当前元素的下一个元素呢?更详细地讲,next 方法的名字应该是这样的——returnCurrentElementAndAdvanceToNextPosition

也就是说,next 方法是『返回当前的元素,并指向下一个元素』。

还容易弄错『最后一个』

在 Iterator 模式中,不仅容易弄错『下一个』,还容易弄错『最后一个』。hasNext 方法在返回最后一个元素前会返回 true,当返回了最后一个元素后则返回 false。

迭代器的种类多种多样

在示例程序中展示的 Iterator 类只是很简单地从前向后遍历集合。其实,遍历的方法是多种多样的。

  • 从最后开始向前遍历。
  • 既可以从前向后遍历,也可以从后向前遍历(既有 next 方法也有 previous 方法)。
  • 指定下标进行『跳跃式』遍历。

具体实现依照需求而定。

拓展

习题

在示例程序的 BookShelf 类中,当书的数量超过最初指定的书架容量时,就无法继续向书架中添加书本了。请大家不使用数组,而是用 java.util.ArrayList 修改程序,确保当书的数量超过最初指定的书架容量时也能继续向书架中添加书本。

答案

github 地址

引用