迭代子模式

迭代子(Iterator)模式

动机(Motivation)

在软件构建过程中,集合对象内部结构常常变化各异,但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为同一种算法在多种集合对象上进行操作提供了可能。

使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。

意图(Intent

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

迭代抽象:

访问一个聚合对象的内容而无需暴露它的内部表示。

迭代多态:

为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。

迭代器的健壮性考虑:遍历的同时更改迭代器所在集合结构,会导致问题。

迭代子(Iterator)模式又叫游标(Cursor)模式,是对象的行为模式。迭代子模式可以顺序地访问一个聚集中的元素而不必暴漏聚集的内部表象。

什么是迭代子模式:

迭代子模式的简略类图如下图所示:

 

聚集和Java聚集

多个对象聚在一起形成的总体称之为聚集(Aggregate),聚集对象是能够包容一组对象的容器对象。聚集依赖于聚集结构的抽象化,具有复杂性和多样性,数组就是最基本的聚集,也是其他Java聚集对象的设计基础。

Java聚集(Collection)对象是实现了共同的java.util.Collection接口的对象,是Java语言对聚集的概念的直接支持。

为什么聚集需要迭代子

聚集对象必须提供适当的方法,允许客户端按照一个线性顺序遍历所有元素对象 ,把元素对象提取出来或者删除掉等。一个使用聚集的系统必然会使用这些方法操作聚集对象,因而在使用聚聚的系统演化过程中,会出现两类情况。

  迭代逻辑没有改变,但是需要将一种聚集对象换成另一种聚集,因为不同的聚集具有不同的遍历接口,所以需要修改客户端代码,以便将已有的迭代调用换成新聚集对象所要求的接口。

  聚集不改变,但是迭代方式需要改变,比如原来只需要读取元素和删除元素,但现在需要增加新的;或者原来的迭代仅仅遍历所有的元素,而现在则需要对元素加以过滤等。这时就只好修改聚集对象,修改已有的遍历方法,或者增加新的方法。

显然,出现这种情况是因为所涉及的聚集设计不符合“开-闭”原则,也就是因为没有将不变的结构从系统中抽象出来,与可变成分分割,并将可变部分的各种实现封装起来。一个聪明的做法无疑是应当使用更加抽象的处理方法,使得在进行迭代时,客户端根本无需知道所使用的聚集是哪个类型;而当客户端需要使用全新的迭代逻辑时,只需要引进一个新的迭代子对象即可,根本无需修改聚集对象本身。

迭代子模式模式便是这样的一个抽象化的概念,这一模式之所以能够做到这一点,是因为它将迭代逻辑封装到一个独立的迭代子对象汇总,从而与聚集本身分隔开。迭代子对象是对遍历的抽象化,不同的聚集对象可以提供相同的迭代子对象,从而使客户端无需知道聚集的低层结构,一个聚集可以提供多个不同的迭代子对象,从而使得遍历逻辑的变化不会影响到聚集对象本身。

按照迭代的方向,迭代可以分为两种:一种为正迭代(Forward Iteration),一种为逆迭代(Backward Iteration)。正迭代按照一个列的正方向尽心迭代;而逆迭代是按照一个列的反方向进行迭代。

 

  抽象迭代子(Iterator)角色:此抽象角色给出了遍历元素所需要的接口。

  具体迭代子(CollectionIterator)角色:此角色实现了Iterator接口,并保持迭代过程中的游标位置。

  聚集(Aggregate)角色:此抽象角色给出创建迭代子(Iterator)对象的接口。

  具体聚集(ConcreteAggregate)角色:实现了迭代子(Iterator)对象的接口,返回一个合适的具体迭代子实例。

  客户端(Client)角色:持有对聚集及迭代子对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作聚集元素的增加和删除。

宽接口和窄接口

如果一个聚集的接口提供了可以用来修改聚集元素的方法,这个接口就是所谓的宽接口;如果一个聚集的接口没有提供修改聚集元素的方法,这样的接口就是所谓的窄接口。

如果聚集对象为所有对象提供同一个接口,也就是宽接口的话,当然会满足迭代子模式对迭代子对象的要求。但是,这样会破坏对聚集对象的封装。这种提供宽接口的聚集叫做白箱聚集。聚集对象向外界提供同样的宽接口。

由于聚集自己实现迭代逻辑,并向外部提供适当的接口,使得子可以从外部控制聚集元素的迭代过程。这样一来迭代子所控制的仅仅是一个游标而已,这种迭代子叫做游标迭代子(Cursor Iterator)

由于迭代子时在聚集的结构之外的,因此这样的迭代子又叫外禀迭代子(Extrinsic Iterator)

在改良的设计中,聚集对象为迭代子对象提供一个宽接口,而为其他对象提供一个窄接口。换言之,聚集对象的内部结构应当对迭代子对象适当公开,以便迭代子对象能够对聚集对像有足够的了解,从而可以进行迭代操作。但是,聚集对象应当避免向其他的对象提供这些方法,因为其他对象应当经过迭代子对象进行这些工作,而不是直接操控聚集对象。在备忘录模式中,有类似的双重接口的做法。

Java语言中,实现双重接口的办法就是将迭代子类设计成聚集类的内部成员类。这样迭代子对象将可以像聚集对象的内部成员一样访问聚集对象的内部结构。这种同时保证聚集对象的封装和迭代子功能的实现的方案叫做暗箱实现方案。

由于迭代子时聚集的内部类,迭代子可以自由访问聚集的元素,所以迭代子可以自行实现迭代功能并控制对聚集元素的迭代逻辑。由于迭代子是在聚集的结构之内定义的,因此这样的迭代子又叫内禀迭代子(Intrinsic Iterator)

白箱聚集与外禀迭代子

一个白箱聚集像外界提供访问自己内部元素的接口(称做遍历方法或者Traversing Method),从而使外禀迭代子可以通过聚集的遍历方法实现迭代功能。

因为迭代的逻辑时由聚集对象本身提供的,所以这样的外禀迭代子角色往往仅仅保持迭代的游标位置。

一个典型的由白箱聚集和外部迭代子组成的系统如下:在这个实现中具体迭代子角色是一个外部类,而具体聚集角色向外界提供遍历聚集元素的接口。

 

暗箱聚集和内禀迭代子:

一个黑箱聚集不向外部提供遍历自己元素对象的接口,因此这些元素对象只可以被聚集内部成员访问,由于内禀迭代子恰好是聚集内部的成员子类,因此,内禀迭代子对象是可以访问聚聚的元素的。

 

静态迭代子和动态迭代子

静态迭代子由聚集对象创建,并持有聚集对象的一个快照(snapshot),在产生后这个快照的内容就不再变化。客户端可以继续修改原聚集的内容,但是迭代子对象不会反映出聚集的新变化。

静态迭代子的好处是它的安全性和简易性,换言之,静态迭代子易于实现,不容易出现错误,但是静态迭代子将原聚集复制了一份,因此它的短处是对时间和内存资源的消耗。对大型聚集对象来说,使用静态迭代子不是一个合适的选择。

动态迭代子则与静态迭代子完全相反,在迭代子被产生之后,迭代子保持着对聚集元素的引用,因此任何对原聚集内容的修改都会在迭代子对象上反映出来。

完整的动态迭代子不容易实现,但是简化的动态迭代子并不难实现。大多数Java设计师遇到的迭代子都是这种简化的动态迭代子,为了说明什么是简化的动态迭代子,首先需要介绍一个新的概念:Fail Fast.

Fail Fast

如果当一个算法开始之后,它的运算环境发生变化,使得算法无法进行必要的调整时,这个算法就应当立即发出故障信号,这就是Fail Fast的含义。

如果聚集对象的元素在一个动态迭代子的迭代过程中发生变化时,迭代过程会受到影响而变得不能自恰,这时候,迭代子就应当立即抛出一个异常。这种迭代子就是实现了Fail Fast功能的迭代子。

何时使用内禀迭代子和外禀迭代子

内禀迭代子是定义在聚集结构内部的迭代子,而外禀迭代子是定义在聚集结构外部的迭代子。Java语言的设计师在设计AbstractList类时,选择了使用内禀迭代子类,也即Itr.但同时这个类也向外部提供自己遍历方法,换言之,如果设计师使用AbstractList聚集,也同样可以定义自己的外禀迭代子。

一个外禀迭代子往往仅存储一个游标,因此如果有介个客户端同时进行迭代的话,那么可以使用几个外禀迭代子对象,由每一个迭代子对象光控制一个独立的游标,但是,外禀迭代子要求聚集对象向外界提供遍历方法,因此会破坏聚集的封装,如果某一个客户端可以修改聚集元素的话,迭代会给出不自恰的结果,甚至影响到系统其他部分的稳定性,造成系统的崩溃。使用外禀迭代子的一个重要理由是它可以被几个不同的方法和对象共同享用和控制。使用内部迭代子的优点是它不破坏对聚集的封装。

 

 

迭代子模式的优点和缺点:

迭代子模式的优点:

  迭代子模式简化了聚集的界面。迭代子具备了一个遍历接口,这样聚集的接口就不必具备遍历接口。

  每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的,因此,一个聚集对象可以同时又几个迭代在进行之中。

  由于遍历算法被封装在迭代子角色里面,因此,迭代的算法可以独立于聚集的角色变化。由于客户端拿到的是一个迭代子对象,因此,不必知道聚集对象的类型,就可以读取和遍历聚集对象。这样即便聚集对象的类型发生变化,也不会影响到客户端的遍历过程。

迭代子模式的缺点:

  迭代子模式给客户端一个聚集被顺序化的错觉,因为大多数的情况下聚集的元素并没有确定的顺序。但是迭代必须以一定的顺序进行,如果客户端误以为顺序是聚集本身的特性而过度以赖于聚集元素的顺序,很可能得出错误的结果。

  迭代子给出的聚集元素没有类型的特征。一般而言,迭代子给出的元素都是Object类型,因此客户端必须具备这些元素类型的知识才能使用这些元素。

 

EnumerationIterator

EnumerationJava1.2版本之前使用的迭代接口,Java1.2版本给出了全新的Java聚集概念,与这个概念同时给出的是:IteratorListIterator接口。

虽然Java的文档不鼓励设计师使用Iterator,而不要使用Enumeration,但是已有的各种在Java1.2之前设计的系统仍然提供Enumeration类型。那么一个自然的问题就是:EnumerationIteratorIterator以及ListIterator这三种迭代接口以及实现到底有什么样的区别呢?

从接口上的区别:

从接口上看,Enumeration没有remove方法,而Iterator接口声明了这一方法。remove()方法允许客户端能够在迭代过程当中将刚刚读取出的聚聚元素删除掉。

Enumeration相比较,ListIterator接口则给出了更多的方法,后者的额外功能主要体现在两个方面:

1.    对反向迭代的支持。ListIterator接口所提供的方法允许客户端在两个方向上迭代,包括正向和反向。

2.    对增加聚集元素和删除聚集元素的支持。

显然,Enumeration给出的是一个最小化的迭代子接口,而IteratorListIterator接口都比它大。

Enumeration不支持FailFast

换言之,假设一个迭代过程是使用Enumeration进行的,那么如果在迭代过程中,聚集对象被外界意外直接修改(不是通过迭代子自己提供的修改方法进行),则这个迭代过程不会立即捕获任何异常,但是,由于聚集内容已经被修改,所以迭代很可能会在最后遇到不能自恰的内容时崩溃。由于这个原因,建议尽量不要使用Enumeration对象,应当尽量使用Iterator对象。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(java,算法,object,iterator,文档,语言)