目录
1. 模式概述
1.1 定义
迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern),提供一种方法顺序访问一个聚合中的各个元素,而不暴露其内部的表示。
通俗的说就是通过对聚合提供额外的遍历类(迭代类),该类主要封装了对聚合的遍历,这么做的好处简化了聚合类并提供了对聚合类统一的遍历方式。
- HeadFirst中形象的表示如下图:
1.2 应用场景
- 1. 访问一个聚合对象的内容而无须暴露它的内部表示。
- 2. 需要为聚合对象提供多种遍历方式。 对于复杂的树或图结构,例如针对图的遍历,可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。
- 3. 为遍历不同的聚合结构提供一个统一的接口。
1.3 优缺点
- 优点:
- 封装聚合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
- 将聚合对象的遍历操作从聚合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
- 让添加新的遍历算法更加容易,更符合“开闭原则”。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。
- 缺点:由于将遍历功能与聚合对象进行抽离,因此对应的类数目会成对增加。
2 迭代器模式原理
通过对聚合类的遍历进行封装成一个迭代类,然后提供统一的遍历方式。
这么做的好处是将遍历与聚合实现了解耦,使得聚合与遍历的设计更加符合“单一职责”设计原则。
当然这么也造成了类的成对增加,毕竟每添加一种聚合就得提供一个相对应的迭代类。
根据迭代器的原理,以及“基于接口编程而非实现编程”,简易的类关系如下
- 说明:迭代器类与具体的聚合实现类是通过组合的方式从而实现了迭代器遍历功能。
3. 以Java为例剖析迭代器
通常对于迭代器模式大多语言将其做为基础类库提供了,比如Java就提供了直接可用的迭代器类。
如下以ArrayList为例说明
在ArrayList源码前,先思考下迭代器如何应对集合的增删问题?
示例代码(以下代码全为与迭代器相关的核心代码,详细可参考JDK源码):
- 1. ArrayList类
public class ArrayList<E> extends AbstractList<E> implements List<E> {
//...
public Iterator<E> iterator() {
return new Itr();
}
}
- 对于具体的聚合实现类主要是提供一个返回对应聚合迭代器类便可;
- 注意ArrayList返回的Itr是ArrayList的内部类
2. Iterator接口类
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
3. ArrayList对应的具体迭代器类
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
解说:
- 1. 上述代码可知道ArrayList维护了一个modCount变量,该变量为ArrayList内部维护的一个记录了集合改变的次数,如下代码删除或添加元素时,该变量会添加一;
ArrayList部分源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
...
}
public E remove(int index) {
...
modCount++;
}
- 2. 在创建迭代器的时候 int expectedModCount = modCount; 这行代码就将集合的修改次数赋值给expectedModCount 所期望的修改次数。
- 3. 在迭代器调用next或remove方法时会先调用该方法checkForComodification();判断expectedModCount与modCount是否相等,不相等则抛出异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 4. 通过上面的分析可知,在应用迭代器遍历的时候是不允许对集合进行增删的,因为增删后集合所维护的成员变量modCount会与迭代器所维护的expectedModCount不一致,从而导致抛异常。为什么要维护这两个变量一致呢?
关于这点稍微思考下其实很容易理解,例如原来获取下标为2的元素,结果在准备获取的时候集合将下标为1的元素删除,那么由于数组会进行移动,那么这时再获取下标为2的元素则不再是原先要获取的元素了。
- 5. 如果非要在遍历的时候对元素进行删除可以直接通过调用迭代器内部的删除方法,源码如下
迭代器Itr类内部的删除方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
- 观察上述代码可知,如果调用迭代器内部的删除方法,其内部是会对集合期望的修改值与集合修改值进行同步更新维护的。