迭代子模式又叫做游标模式。迭代子模式可以顺序地访问一个聚集中的元素而必暴露聚集的内部表象。
1. 聚集和Java聚集
多个对象在一起形成的总体形成聚集(Aggregate),聚集对象是能够包容一组对象的容器对象。数组就是最基本的聚集,也是其他Java聚集对象的设计基础。
Java聚集(Collection)对象是实现了共同的接口(java.util.Collection)的对象。从1.2版本开始提供了很多聚集,包括Vector、ArrayList、Queue、Stack、LinkedList、HashSet、TreeSet、HashTable、Hashmap以及TreeMap等。
2. 为什么需要迭代子
迭代子对象是对遍历的抽象化,不同的聚集对象可以提供相同的迭代子对象,从而使客户端无需知道聚集的低层结构。它将迭代逻辑封装到一个独立的迭代子对象中,从而与聚集本身分隔开。一个聚集可以提供多个不同的迭代子对象,从而使得遍历逻辑的变化不会影响到聚集对象本身。
从代码重构的角度看,迭代子在客户端和聚集层之间增加了一层,从而使得客户端与聚集之间的通信从直接变为间接。这么做的好处是缓存了客户端的变化对聚集的影响,以及聚集的变化对客户端的影响。
3. 迭代子模式的结构
一般性结构如下:
涉及的角色如下:
抽象迭代子(Iterator)角色:此抽象角色定义出遍历元素所需的接口
具体迭代子(ConcreteIterator)角色:实现了Iterator接口,并保持迭代过程中的游标位置。
聚集(Aggregate)角色:此抽象角色给出创建迭代子对象的接口。
具体聚集(ConcreteAggregate)角色:实现了创建迭代子对象的接口,返回一个合适的具体迭代子实例。
客户端角色:持有对聚集及其迭代子对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作聚集元素的增加和删除。
宽接口和窄接口:
如果一个具体的接口提供了可以用来修改聚集元素的方法,这个接口就是宽接口;反之则为窄接口。
提供宽接口的聚集又被称为白箱聚集。聚集对象向外界提供相同的宽接口,如下:
由于迭代子是在聚集的结构之外的,因此这样的迭代子又被称为外禀迭代子模式。
在改良的设计中,聚集对象为迭代子对象提供宽接口,对其他对象提供窄接口。换言之,聚集对象的内部结构对迭代子应当适当的公开。
由于迭代子是在聚集的结构内定义的,因此这样的迭代子又称为内禀迭代子。
3.1 白箱聚集与外禀迭代子
白箱聚集向外界提供访问自己内部元素的接口,从而使外禀迭代子可以通过聚集的遍历方法实现迭代功能。
结构图如下:
源代码如下:
抽象聚集角色类,这个角色规定出所有的具体聚集必须实现的接口。迭代子模式要求聚集对象必须有一个工厂方法,也就是createIterator()方法,以向外界提供迭代子对象的实例。
public abstract class Aggregate { /** * 工厂方法,创建相应迭代子对象的接口 */ public abstract Iterator createIterator(); }
抽象迭代子角色声明具体迭代子需要实现的接口
public interface Iterator { /** * 迭代方法:移动到第一个元素 */ void first(); /** * 迭代方法:移动到下一个元素 */ void next(); /** * 迭代方法:是否为最后一个元素 */ boolean isDone(); /** * 迭代方法:返还当前元素 */ Object currentItem(); }
具体聚集角色实现抽象聚集角色类所要求的接口,也就是createIterator()方法。此外,还有方法getElement()向外界提供聚集元素,而方法size()向外界提供聚集的大小等。
public class ConcreteAggregate extends Aggregate { private Object[] objArray = null; /** * 构造方法,传入聚合对象的具体内容 */ public ConcreteAggregate(Object[] objArray) { this.objArray = objArray; } @Override public Iterator createIterator() { return new ConcreteIterator(this); } /** * 取值方法:向外界提供聚集元素 */ public Object getElement(int index) { if (index < objArray.length) { return objArray[index]; } else { return null; } } /** * 取值方法:向外界提供聚集的大小 */ public int size() { return objArray.length; } }
具体迭代子角色
public class ConcreteIterator implements Iterator { // 持有被迭代的具体的聚合对象 private ConcreteAggregate concreteAggregate; // 内部索引,记录当前迭代到的索引位置 private int index = 0; // 记录当前聚集对象的大小 private int size = 0; public ConcreteIterator(ConcreteAggregate agg) { this.concreteAggregate = agg; this.size = agg.size(); index = 0; } /** * 迭代方法:返还当前元素 */ @Override public Object currentItem() { return concreteAggregate.getElement(index); } /** * 迭代方法:移动到第一个元素 */ @Override public void first() { index = 0; } /** * 迭代方法:是否为最后一个元素 */ @Override public boolean isDone() { return (index >= size); } /** * 迭代方法:移动到下一个元素 */ @Override public void next() { if (index < size) { index++; } } }
客户端类:
public class Client { public void operation() { Object[] objArray = { "1", "2", "3", "4", "5" }; Aggregate agg = new ConcreteAggregate(objArray); Iterator it = agg.createIterator(); while (!it.isDone()) { System.out.println(it.currentItem()); it.next(); } } public static void main(String[] args) { Client client = new Client(); client.operation(); } }
结果:
1
2
3
4
5
外禀迭代子的意义:
一个常常会问的问题是:既然白箱聚集已经向外界提供了遍历方法,客户端已经可以自行进行迭代了,为什么还要应用迭代子模式,并创建一个迭代子对象进行迭代呢?
客户端当然可以自行进行迭代,不一定非得需要一个迭代子对象。但是,迭代子对象和迭代模式会将迭代过程抽象化,将作为迭代消费者的客户端与迭代负责人的迭代子责任分隔开,使得两者可以独立的演化。在聚集对象的种类发生变化,或者迭代的方法发生改变时,迭代子作为一个中介层可以吸收变化的因素,而避免修改客户端或者聚集本身。
此外,如果系统需要同时针对几个不同的聚集对象进行迭代,而这些聚集对象所提供的遍历方法有所不同时,使用迭代子模式和一个外界的迭代子对象是有意义的。具有同一迭代接口的不同迭代子对象处理具有不同遍历接口的聚集对象,使得系统可以使用一个统一的迭代接口进行所有的迭代
3.2黑箱聚集与内禀迭代器 (只是把具体迭代子角色设计为具体聚集角色的私有内部成员内)
一个黑箱聚集不向外部提供遍历自己元素的接口,因此这些元素对象只可以被聚集内部成员访问。由于内禀迭代子恰好是聚集内部的成员子类,因此内禀迭代子对象是可以访问聚集的元素的。
结构图如下:
源代码如下:(只是具体迭代子角色和具体聚集角色代码与上面不同)
public abstract class Aggregate { /** * 工厂方法,创建相应迭代子对象的接口 */ public abstract Iterator createIterator(); }
public interface Iterator { /** * 迭代方法:移动到第一个元素 */ void first(); /** * 迭代方法:移动到下一个元素 */ void next(); /** * 迭代方法:是否为最后一个元素 */ boolean isDone(); /** * 迭代方法:返还当前元素 */ Object currentItem(); }
具体聚集角色将具体迭代子封装为私有内部成员类。
public class ConcreteAggregate extends Aggregate { private Object[] objArray = null; /** * 构造方法,传入聚合对象的具体内容 */ public ConcreteAggregate(Object[] objArray) { this.objArray = objArray; } @Override public Iterator createIterator() { return new ConcreteIterator(); } /** * 内部成员类,具体迭代子类 */ private class ConcreteIterator implements Iterator { // 内部索引,记录当前迭代到的索引位置 private int index = 0; // 记录当前聚集对象的大小 private int size = 0; /** * 构造函数 */ public ConcreteIterator() { this.size = objArray.length; index = 0; } /** * 迭代方法:返还当前元素 */ @Override public Object currentItem() { return objArray[index]; } /** * 迭代方法:移动到第一个元素 */ @Override public void first() { index = 0; } /** * 迭代方法:是否为最后一个元素 */ @Override public boolean isDone() { return (index >= size); } /** * 迭代方法:移动到下一个元素 */ @Override public void next() { if (index < size) { index++; } } } }
客户端代码:
public class Client { public void operation() { Object[] objArray = { "1", "2", "3", "4", "5" }; Aggregate agg = new ConcreteAggregate(objArray); Iterator it = agg.createIterator(); while (!it.isDone()) { System.out.println(it.currentItem()); it.next(); } } public static void main(String[] args) { Client client = new Client(); client.operation(); } }
结果:
1
2
3
4
5
4. 迭代子模式的实现
4.1 主动迭代子和被动迭代子
迭代子是主动的还是被动的是相对于客户端而言的。如果客户端控制迭代的进程,那么这样的迭代子就是主动迭代子,相反就是被动迭代子。
使用主动迭代子的客户端会明显调用迭代子的next()等方法,在遍历过程中向前行进;而客户端在调用被动迭代子模式时,客户端不需要明显的调用迭代方法,迭代子自行推进遍历过程。
4.2 静态迭代子和动态迭代子
静态迭代子由聚集对象创建,并持有聚集对象的一个快照(snapshot),在产生后这个快照的内容不再变化。客户端可以继续修改原聚集的内容,但是迭代子不会反映出来。其好是安全性和简易性。但是由于静态迭代子将原聚集内容复制了一份,其缺点是对时间和内存资源的浪费。
动态迭代子与静态迭代子相反,在迭代子被产生之后,迭代子保持对聚集元素的引用,因此任何对原聚集内容的改变都会在迭代子对象上反映出来。完整的动态迭代子不容易实现,但是简易的动态迭代子易于实现。大多数java设计师遇到的迭代子都是简化的动态迭代子。
为了说明什么是动态迭代子,需要先说明一个新概念: Fail Fast
Fail Fast:
如果一个算法开始之后,它的运算环境发生变化,使得算法无法进行必需的调整时,这个算法就应当立即发出故障信号。这就是Fail Fast的含义。
如果聚集对象的元素在一个动态迭代子的迭代过程中发生变化时,迭代过程会受到影响而变得不能自恰。这时候,迭代子就应当立即抛出一个异常。这种迭代子就是实现了Fail Fast功能的迭代子。
Fail Fast 是一种出现错误时的早期报警功能。如果迭代子遍历的对象被外界直接修改会造成迭代崩溃,这时候迭代子应当在崩溃可能发生之前尽早抛出一个异常。
Abstract.Itr迭代子是一个FailFast的迭代子,方法 checkForComodification 会检查聚集的内容是否刚刚被外界直接修改过(不是通过迭代子的 remove() 方法修改的)。如果在迭代后,聚集的内容被外界直接修改的话会立即抛出ConcurrentModificationException异常。(Java中LIst的迭代模式也是基于黑箱子的迭代子模式,将具体迭代设为内部类)
源码如下:
private class Itr implements Iterator<E> { /** * Index of element to be returned by subsequent call to next. */ int cursor = 0; /** * Index of element returned by most recent call to next or * previous. Reset to -1 if this element is deleted by a call * to remove. */ int lastRet = -1; /** * The modCount value that the iterator believes that the backing * List should have. If this expectation is violated, the iterator * has detected concurrent modification. */ int expectedModCount = modCount; public boolean hasNext() { return cursor != size(); } public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
4.3 迭代子模式的优缺点
优点:
(1)迭代子模式简化了聚集的接口。迭代子具备了一个遍历接口,这样聚集的接口就不必具备遍历接口。
(2)每一个聚集对象都可以有一个或多个迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。因此,一个聚集对象可以同时有几个迭代在进行之中。
(3)由于遍历算法被封装在迭代子角色里面,因此迭代的算法可以独立于聚集角色变化。
缺点:
(1)迭代子模式给客户端一个聚集被顺序化的错觉,因为大多数的情况下聚集的元素并没有确定的顺序,但是迭代必须以一定的顺序进行。
(2)迭代子给出的聚集元素没有类型特征。一般而言,迭代子给出的元素都是Object类型。 (这个在目前的聚集中已通过泛型解决)
总结:
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口:hasNext, next。
应用实例:JAVA 中的 iterator。
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。