迭代子模式是设计模式中最常见的几个设计模式之一,也是被广泛地应用到java语言API 中的几个设计模式之一。
集合和java集合
多个对象聚在一起形成的总体称之为集合,集合对象是能够包容一组对象的容器对象。集合依赖于集合结构的抽象化,具有复杂性和多样性。数组就是最基本的集合,也是其他java集合对象的设计基础。
java集合对象是实现了共同java.util.Collection接口的对象,是java语言对集合概念的直接支持。从1.2版本开始,java语言提供了很多种集合,包括Vector、ArrayList、Queue、Stack、LinkedList、HashSet、TreeSet、Hashtable、HashMap以及TreeMap等,这些都是java集合的例子。
为什么集合需要迭代子
集合对象必须提供适当的方法,允许客户端能够按照一个线性顺序遍历所有的元素对象,把元素对象提取出来或者删除掉等。一个使用集合的系统必然会使用这些方法操控集合对象,因而在使用集合的系统演化过程中会出现两类问题:
(1)迭代逻辑没有改变,但是需要将一种集合换成另一种集合。因为不同的集合具有不同的遍历接口,所以需要修改客户端代码,以便将已有的迭代调用换成新集合对象所要求的接口。
(2)集合不会改变,但是迭代方式需要改变。比如原来只需要读取元素和删除元素,但是现在需要增加新的元素;或者原来的迭代仅仅遍历所有的元素,而现在则需要对元素加以过滤等。这时就只好修改集合对象,修改已有的遍历,胡总增加新的方法。
显然,出现这种情况是因为所涉及的集合设计不符合“开-闭”原则,也就是因为没有将不变的结构从系统中抽象出来,与可变成分分割,并将可变部分的各种实现封装起来。一个聪明的做法无疑是应当使用更加抽象的处理费方法,使得在进行迭代时,客户端根本无需知道所使用的集合是那个类型;而当客户端需要使用全新的迭代逻辑时,只需引进一个新的迭代子对象即可,根本无需修改集合对象本身。
迭代子模式便是这样的一个抽象化的概念。这一模式之所以能够做到这一点,是因为它将迭代逻辑封装到一个独立的迭代子对象中,从而与集合本身分割开。迭代子对象是对遍历的抽象化,不同的集合对象可以提供相同的迭代子对象,从而使客户端无需知道集合的底层结构。一个集合可以提供多个不同的迭代子对象,从而使得遍历逻辑的变化不会影响到集合对象本身。
“开- 闭 ”原则
“开闭”原则要求系统可以在不修改已有代码的前提下,进行功能扩展,做到这一点的途径就是对变法封装。
从对变化的封装角度讲,迭代子模式将访问集合元素的逻辑封装起来,并且使它独立于集合对象的封装。这就提供了集合存储逻辑和迭代逻辑独立演变的空间,增加了系统的可复用性。这样系统更加符合“开-闭”原则的要求,可以使系统具有在无需修改的情况下进行扩展的能力。
迭代子模式的结构
一般性结构
迭代子模式的一般性结构图如下图所示:
迭代子模式涉及到以下几个角色:
(1) 抽象迭代子(Iterator ) 角色:此抽象角色定义出遍历元素所需的接口。
(2) 具体迭代子(ConcreteIterator ) 角色:此角色实现了Iterator接口,并保持迭代过程中的游标位置。
(3) 集合(A ggregate) 角色 :此抽象角色给出创建迭代子(Iterator)对象的接口。
(4) 具体集合(ConcreteAggregate ) 角色:实现了创建迭代子(Iterator)对象的接口,返回一个合适的具体迭代子实例。
(5) 客户端(Client ) 角色:持有对集合及其迭代子对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作集合元素的增加和删除。
宽接口和窄接口
如果一个集合的接口提供了可以用来修改集合元素的方法,这个接口就是所谓的宽接口,如果一个集合的接口没有提供修改集合元素的方法,那么这个接口就是窄接口。
如果集合对象为所有对象提供同一个接口,也就是宽接口的话,当然会满足迭代子模式对迭代子对象的要求。但是这样会破坏对集合对象的封装。这种提供宽接口的集合叫做白箱集合。集合对象对外界提供同样的宽接口,如果下图所示:
由于集合自己实现迭代逻辑,并向外部提供适当的接口,使得迭代子可以从外部控制集合元素的地带过程。这样一来迭代子所控制的仅仅是一个游标而已,这种迭代子叫做游标迭代子(Cursor Iterator)。
由于迭代子是在集合的结构之外的,因此这样的迭代子又叫做外禀迭代子( Extrinsic Iterator)。
在改良的设计中,集合对象为迭代子对象提供一个宽接口,而为其他对象提供一个窄接口。集合对象的内部结构应当对迭代子对象适当的公开,以便迭代子对象能够对集合对象有足够的了解,从而可以进行迭代操作。但是集合对象应当避免向其他的对象提供这些方法,因为其他对象应当经过迭代子对象进行这些工作,而不是直接操控集合对象。集合对象向外界提供两种不同的接口,如下图所示:
在java语言中,实现双重接口的办法就是将迭代子类设计成聚集类的内部成员类。这样迭代子对象将可以像集合对象的内部成员一样访问集合对象的内部结构。这种同时保证集合对象的封装好迭代子功能的实现的方案叫做黑箱实现方案。
由于迭代子是集合的内部类,迭代子可以自由访问集合的元素,所以迭代子可以自行实现迭代功能并控制对集合元素的迭代逻辑。由于迭代子是在集合的结构之内定义的,因此这样的迭代子又叫做内禀迭代子(Intrinsic Iterator)。
白箱集合和外禀迭代子
首先看白箱集合和外禀迭代子的实现。一个白箱集合向外界提供访问自己内部),从而使得外禀迭代子可以通过集合的遍历方法实现迭代功能。
本身提供,所有这样的外禀迭代子角色往往仅仅保存迭代的游标位置。
一个典型的白箱集合和外禀迭代子组成的系统如下所示,在这个实现中具体迭代子角色是一个外部类,而具体集合角色像外界提供遍历集合元素的接口。
下面是一个简单示例的源码:
1.Iterator.java
package com.pattem.behavioral.iterator;
/**
* 抽象迭代子角色
* */
public interface Iterator {
//迭代方法:移动到第一个元素
public void first();
//迭代方法:移动到下一个元素
public void next();
//迭代方法:是否是最后一个元素
public boolean isDone();
//迭代方法:返还当前元素
public Object currentItem();
}
2.Aggregate.java
package com.pattem.behavioral.iterator;
/**
* 抽象集合角色
* */
public abstract class Aggregate {
//返还一个迭代子对象
public Iterator createIterator(){
return null;
}
}
3.ConcreteIterator.java
package com.pattem.behavioral.iterator;
/**
* 具体迭代子角色
* */
public class ConcreteIterator implements Iterator {
private ConcreteAggregate agg;
private int index =0;
private int size =0;
//构造函数
public ConcreteIterator(ConcreteAggregate agg){
this.agg = agg;
size = agg.size();
index = 0;
}
//迭代方法,移动到第一个元素
public void first() {
index = 0;
}
//迭代方法,移动到下一个元素
public void next() {
if(index<size){
index++;
}
}
//迭代子方法,是否是最后一个元素
public boolean isDone() {
return (index==size-1);
}
public Object currentItem() {
return agg.getElement(index);
}
}
4.ConcreteAggregate.java
package com.pattem.behavioral.iterator;
/**
* 具体集合角色
* */
public class ConcreteAggregate extends Aggregate{
private Object [] obj = {"Monk Tang","Monkey","Pigsy","Sandy","Horse"};
//工厂方法,返还一个迭代子
public Iterator createIterator(){
return new ConcreteIterator(this);
}
//取值方法:向外界提供集合元素
public Object getElement(int index){
if(index<obj.length){
return obj[index];
}
else
return null;
}
//取值方法
public int size(){
return obj.length;
}
}
5.Client.java
package com.pattem.behavioral.iterator;
public class Client {
private Iterator it;
private Aggregate agg = new ConcreteAggregate();
public void operation(){
it = agg.createIterator();
while(!it.isDone()){
System.out.println(it.currentItem().toString());
it.next();
}
}
public static void main(String[] args) {
Client client = new Client();
client.operation();
}
}
外禀迭代子的意义
既然白箱聚集已经向外界提供了遍历方法,客户端已经可以自行进行迭代了,为什么还要应用迭代子模式,并创建一个迭代子对象进行迭代呢?
客户端当然可以自行进行迭代,不一定非得需要一个迭代子对象。但是,迭代子对象和迭代模式会将迭代过程抽象化,将作为迭代消费者的客户端与迭代负责人的迭代子责任分隔开,使得两者可以独立演化。在集合对象的种类发生变化,或者迭代的方法发生变化时,迭代子作为一个中介层可以吸收变化的因素,而避免修改客户端或者集合本身。
此外,如果系统需要同时针对几个不同的集合对象进行迭代,而这些集合对象所提供的遍历方法有所不同时,使用迭代子模式和一个外界的迭代子对象是有意义的。具有同一迭代接口的不同迭代子对象处理具有不同遍历接口的集合对象,使得系统可以使用一个统一的迭代接口进行所有的迭代。
黑箱集合和内禀迭代子
一个黑箱集合不向外部提供遍历自己元素对象的接口,因此,这些元素对象只可以被集合内部成员访问。由于内禀迭代子恰好是集合内部的成员子类,因此,内禀迭代子对象是可以访问集合的元素的。
为了说明黑箱方案的细节,这里给出一个示意性的黑箱实例,如下图所示:
黑箱和白箱的例子是同一个,只是ConcreteAggregate.java类的实现不同。源码如下:
ConcreteAggregate.java
package com.pattem.behavioral.blackiterator;
public class ConcreteAggregate extends Aggregate{
private Object [] obj = {"Monk Tang","Monkey","Pigsy","Sandy","Horse"};
//工厂方法:返还一个迭代子对象
public Iterator createIterator() {
return new ConcreteIterator();
}
//内部成员类,具体迭代子类
public class ConcreteIterator implements Iterator{
private int currentIndex= 0;
//迭代方法:移动到第一个元素
public void first() {
currentIndex=0;
}
//迭代方法:移动到下一个元素
public void next() {
if(currentIndex<obj.length){
currentIndex++;
}
}
//迭代方法:判断是不是最后一个元素
public boolean isDone() {
return (currentIndex==obj.length-1);
}
//迭代方法:返还当前元素
public Object currentItem() {
return obj[currentIndex];
}
}
}
白箱集合可以是不变对象吗
一个白箱集合对象可能是不变对象,前提是它的内部状态,包括集合元素是不会改变的。
一个白箱集合对象自行向外界提供遍历方法,而外禀迭代子对象必须存储迭代的游标,因此,在迭代过程中白箱集合本身并不存储游标,所以它的状态是可以保持不变的。
黑箱集合可以是不变对象吗
一个黑箱集合对象不可能是不变对象,因为它的内部状态是会改变的。
一个黑箱集合对象自行向外界提供一个内禀迭代子对象,而迭代子对象必须存储迭代的游标,因此,在迭代过程中迭代子对象的状态是会改变的,这以为这集合对象的状态也是会改变的。
迭代子模式的实现
主动迭代子和被动迭代子
迭代子是主动(Active)的还是被动(Passive)的,是相对于客户端而言的。如果客户端控制迭代的进程,那么这样的迭代子就是主动迭代子,反之,则是被动迭代子。
使用主动迭代子的客户端会明显调用迭代子的next()等方法,在遍历过程中向前进行;而客户端在使用被动迭代子时,客户端并不明显地调用迭代方法,迭代子自行推进遍历过程。
何时使用内禀迭代子和外禀迭代子
内禀迭代子是定义在集合结构内部的迭代子,外禀迭代子是定义在集合结构外部的迭代子。
在什么情况下选择内禀迭代子,什么情况下选择外禀迭代子?
一个外禀迭代子往往仅存储一个游标,因此如果有几个客户端同时进行迭代的话,那么可以使用几个外禀迭代子对象,由每一个迭代子对象控制一个独立的游标。但是,外禀迭代子要求集合向外提供遍历方法,因此会破坏对集合的封装。如果某一个客户端可以修改集合元素的话,会影响到系统其它部分的稳定性,甚至造成系统崩溃。
使用外禀迭代子的一个重要理由是它可以被几个不同的方法和对象共同共享和控制,使用内禀迭代子的优点是它不破坏对集合的封装。
迭代子模式的优点和缺点
迭代子模式有如下优点:
(1)迭代子模式简化了集合的界面。迭代子具备了一个遍历接口,这样集合的接口就不必具备遍历接口。
(2)每一个集合对象都可以有一个或一个以上的迭代子对象,每一个迭代子对象的迭代状态是可以彼此独立的。因此一个集合对象可以同时有几个迭代在进行中。
(3)由于遍历算法被封装在迭代子角色里面,因此迭代的算法可以独立于集合角色变化。由于客户端那到底一个迭代子对象,因此不必知道集合对象的类型,就可以读取和遍历集合对象。这使得集合对象的类型发生变化,也不会影响到客户端的遍历过程。
迭代子模式有如下缺点:
(1)迭代子模式给客户端一个集合被顺序化的错觉,因为大多数情况下集合的元素并没有确定的顺序,但是迭代必须以一定的线性顺序进行。如果客户端误以为顺序是集合本身具有的特效而过度依赖集合元素的顺序,可能会得出错误的结果。
(2)迭代子给出的集合元素没有类型特征。一般而言,迭代子给出的元素都是Object类型,因此客户端必须具备这些元素类型的知识才能使用这些元素。