迭代器模式(Iterator)
走遍天下,世界那么大,我想去看看
在计算机中,Iterator意为迭代器,迭代有重复的含义,在程序中,更有“遍历”的含义
如果给定一个数组,我们可以通过for循环来遍历这个数组,这种遍历就叫做迭代
对于数组这种数据结构,我们称为是可迭代的
所以
迭代器就是可以用来对于一个数据集合进行遍历的对象
意图
提供一种方法,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
别名:游标 Cursor
集合与遍历
由一个或多个确定的元素所构成的整体叫做集合。
多个对象聚集在一起形成的总体称之为聚集aggregate。
集合和聚集有相似的含义。
容器是指盛物品的器具,Java的Collection框架,就是设计用来保存对象的容器
容器可以认为是集合、聚集的具体体现形式,三个元素在一起叫做集合(聚集),怎么在一起?数组,列表?这具体的体现形式就是容器
容器必须提供内部对象的访问方式,如果不能获取对象,容器也失去了存在的意义,一个只能进不能出的储蓄罐你要它何用?
因为容器的存在就是为了更方便的使用、管理对象。
而且通常需要容器提供对于内部所有元素的遍历方法。
然而容器其内部有不同的摆放形式,可顺序,可无序堆集
简言之,就是不同类型的容器必然有不同的内部数据结构
那么,一种解决办法就是不同的容器各自提供自己的遍历方法。
这样的话,对于使用容器管理对象的客户端程序来说:
如果迭代的逻辑,也就是业务逻辑没有变化
当需要更换为另外的集合时,就需要同时更换这个迭代方法
考虑这样一个场景
有一个方法,方法的参数类型为 Collection
他的迭代逻辑,也就是业务逻辑为遍历所有元素,读取每个元素的信息,并且进行打印...
如果不同的容器有不同的遍历方法,也就是一种实现类有一种不同的遍历方法
一旦更换实现类,那么就需要同步更换掉这个迭代方法,否则方法将无法通过编译
有一个方法,方法的参数类型为 Collection
他的迭代逻辑,也就是业务逻辑为遍历所有元素,读取每个元素的信息,并且进行打印...
如果不同的容器有不同的遍历方法,也就是一种实现类有一种不同的遍历方法
一旦更换实现类,那么就需要同步更换掉这个迭代方法,否则方法将无法通过编译
如果集合的实现不变,需要改变业务逻辑,也就是迭代的逻辑
那么就需要修改容器类的迭代方法,也就是修改原来的遍历方法
还是上面的场景
有一个方法,方法的参数类型为 Collection
他的迭代逻辑,也就是业务逻辑为遍历所有元素,读取每个元素的信息,并且进行打印...
现在他的实现类无需变化
但是业务逻辑需要变动,比如希望从后往前的方式进行遍历,而不再是从前往后
就需要修改原来的方法或者重新写一个方法
出现上述问题的根本原因就在于元素的迭代逻辑与容器本身耦合在一起
当迭代逻辑或者集合实现发生变更时,需要进行修改,不符合开闭原则
容器自身不仅仅需要存储管理对象,还要负责对象的遍历访问,不符合单一职责原则
存储管理对象是容器的核心职责,虽然经常需要提供遍历方法,但是他并不是核心职责
但是为了提供遍历元素的方法,可能不得不在容器类内提供各种全局变量,比如保存当前的位置等,这无疑也会导致容器聚集类设计的复杂度
结构
抽象迭代器角色Iterator
定义遍历元素所需要的接口
具体的迭代器ConcreteIterator
实现了Iterator接口,并且跟踪当前位置
抽象集合容器角色Aggregate
定义创建相应迭代器的接口(方法)
就是一个容器类,并且定义了一个返回迭代器的方法
具体的容器角色ConcreteAggregate
Aggregate的子类,并且实现了创建Iterator对象的接口,也就是返回一个ConcreteIterator实例
客户端角色Client
持有容器对象以及迭代器对象的引用,调用迭代对象的迭代方法遍历元素
迭代器模式中,通过一个外部的迭代器来对容器集合对象进行遍历。
迭代器定义了遍历访问元素的协议方式。
容器集合对象提供创建迭代器的方法。
示例代码
Aggregate角色
提供了iterator()获取Iterator
package iterator; public abstract class Aggregate { abstract Iterator iterator(); abstract Object get(int index); abstract int size(); }
ConcreateAggregate角色
内部使用一个Object数组,数组直接通过构造方法传递进去(只是为了演示学习模式,不要纠结这算不上一个容器)
提供了大小的获取方法以及获取指定下标元素的方法
尤其是实现了iterator()方法,创建一个ConcreteIterator实例,将当前ConcreteAggregate作为参数传递给他的构造方法
package iterator; public class ConcreateAggregate extends Aggregate { private Object[] objects; ConcreateAggregate(Object[] objects) { this.objects = objects; } @Override Iterator iterator() { return new ConcreateIterator(this); } @Override Object get(int index) { return objects[index]; } @Override int size() { return objects.length; } }
迭代器接口
一个是否还有元素的方法,一个获取下一个元素的方法
package iterator; public interface Iterator { boolean hasNext(); Object next(); }
具体的迭代器
内部维护了数据的大小和当前位置
如果下标未到最后,那么就是还有元素
next()方法用于获取当前元素,获取后当前位置往后移动一下
package iterator; public class ConcreateIterator implements Iterator { private Aggregate aggregate; private int index = 0; private int size = 0; ConcreateIterator(Aggregate aggregate) { this.aggregate = aggregate; size = aggregate.size(); } @Override public boolean hasNext() { return index < size ? true : false; } @Override public Object next() { Object value = aggregate.get(index); index++; return value; } }
测试类
package iterator; public class Client { public static void main(String[] args) { Object[] objects = {"1", 2, 3, 4, 5}; Aggregate aggregate = new ConcreateAggregate(objects); Iterator iterator = aggregate.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }
示例代码中ConcreateAggregate本身提供了获取指定下标元素的方法,可以直接调用获取元素
借助于Iterator,将迭代逻辑从Aggregate中剥离出来,独立封装实现
在客户端与容器之间,增加了一层Iterator,实现了客户端程序与容器的解耦
说白了,增加了Iterator,相当于通过Iterator封装了真实容器对象的获取元素的方法
不直接调用方法,经过Iterator转换一层
而且仔细品味下,这有点“适配”的韵味,适配的目标就是统一的元素访问协议,通过Iterator约定
而被适配的角色,则是真实容器对象元素的操作方法
总之“间接”“委托”“代理”的感觉,对吧,好处自己品味
外部迭代与内部迭代
在上面的示例程序中,通过引入Iterator,实现了迭代逻辑的封装抽象
但是容器聚集对象本身有获取元素的方法,所以客户端仍旧可以自行遍历
Iterator也只不过是容器聚集对象的一个客户而已
这种迭代器也叫做外部迭代器
对于外部迭代器有一个问题,对于不同的ConcreteAggregate,可能都需要一个不同的ConcreteIterator
也就是很可能会不得不创建了一个与Aggregate等级结构平行的Iterator结构,出现了很多的ConcreteIterator类
这势必会增加维护成本
而且,虽然迭代器将客户端的访问与容器进行解耦,但是迭代器却是必须依赖容器对象的
也就是迭代器类ConcreteIterator与ConcreteAggregate必须进行通信,会增加设计的复杂度,而且这也会增加类之间的耦合性
另外的一种方法是使用内部类的形式,也就是将ConcreteIterator的实现,移入到ConcreteAggregate的内部
借助于内部类的优势:对外部类有充足的访问权限,也就是无需担心为了通信要增加复杂度的问题
准确的说,你没有任何的通信成本,内部类可以直接读取外部类的属性数据信息
而且,使用内部类的方式不会导致类的爆炸(尽管仍旧是会有另一个class文件,但是从代码维护的角度看算是一个类)
这种形式可以叫做内部迭代器
不过无论哪种方式,你可以看得出来,使用迭代器的客户端代码,都是一样的
借助于工厂方法iterator()获得一个迭代器实例(简单工厂模式)
然后借助于迭代器进行元素遍历
JDK中的迭代
我们看下JDK中的Collection提供给我们的迭代方式
Collection是所有集合的父类,Collection实现了Iterable接口
Iterable接口提供了iterator()方法用于返回一个Iterator类的一个实例对象
Iterator类提供了对元素的遍历方法
接下来看下ArrayList的实现
ArrayList中iterator()返回了一个Itr对象,而这个对象是ArrayList的内部类,实现了Iterator接口
看得出来,java给集合框架内置了迭代器模式
在ArrayList中使用就是内部类的形式,也就是内部迭代器
boolean hasNext()
是否拥有更多元素,换句话说,如果next()方法不会抛出异常,就会返回true
next();
返回下一个元素
remove()
删除元素
有几点需要注意
1.)初始时,可以认为“当前位置”为第一个元素前面
所以next()获取第一个元素
2.)根据第一点,初始的当前位置”为第一个元素前面,所以如果想要删除第一个元素的话,必须先next,然后remove
Iterator iterator = list.iterator();
iterator.next();
iterator.remove();
否则,会抛出异常
3.)不仅仅是删除第一个元素需要先next,然后才能remove,每一个remove,前面必须有一个next,成对出现
所以remove是删除当前元素
如果下面这样,会抛出异常
iterator.next();
iterator.remove();
iterator.remove();
4.)迭代器只能遍历一次,如果需要重新遍历,可以重新获取迭代器对象
如果已经遍历到尾部之后仍旧继续使用,将会抛出异常
Iterator iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); } iterator.next();
总结
在java中万事万物都是对象
前面的命令模式将请求转换为命令对象
解释器模式中,将语法规则转换为终结符表达式和非终结符表达式
在迭代器模式中,将“遍历元素”转换为对象
通过迭代器模式引入迭代器,将遍历逻辑功能从容器聚集对象中分离出来
聚合对象本身只负责数据存储,遍历的职责交给了迭代器
对于同一个容器对象,可以定义多种迭代器,也就是可以定义多种遍历方式
如果需要使用另外的迭代方式,仅仅需要更改迭代器对象即可
这样你甚至可以把ConcreteIterator使用配置文件进行注入,灵活设置
将迭代遍历的逻辑从容器对象中分离,必然会减少容器类的复杂程度
当增加新的容器类或者迭代器类时,不需要修改原有的代码,符合开闭原则
如果你想要将容器聚集对象的遍历逻辑从容器对象中的分离
或者想要提供多种不同形式的遍历方式时,或者你想为不同的容器对象提供一致性的遍历接口逻辑
你就应该考虑迭代器模式了
迭代器模式的应用是如此广泛,以至于java已经将他内置到集合框架中了
所以对于我们自己来说,多数时候可以认为迭代器模式几乎用不到了
因为绝大多数时候,使用框架提供的应该就足够了
在java实现中,迭代器模式的比较好的做法就是Java集合框架使用的这种形式---内部类形式的内部迭代器,如果真的需要自己搞一个迭代器,建议仿照集合框架搞吧
借助于迭代器模式,如果迭代的逻辑不变,更换另外的集合实现,因为实现了共同的迭代器接口,所以不需要对迭代这块,无需做任何变动
如果需要改变迭代逻辑,必须增加新的迭代形式,只需要增加一个新的内部类实现迭代器接口即可,其他使用的地方只需要做很小的调整
ArrayList中的ListIterator<E> listIterator() 方法就是如此
有人觉得增加一个类和一个方法这不也是修改么?个人认为:开闭原则尽管最高境界是完全的对扩展开放对修改关闭,但是也不能死抠字眼
增加了一个新的获取迭代对象的方法以及一个新的类,总比将原有的源代码中添加新的方法那种修改要强得多,所有的遍历逻辑都封装在新的迭代器实现类中,某种程度上可以认为并没有“修改源代码”
使用内部类的形式,有人觉得不还是在一个文件中么?但是内部类会有单独的class文件,而且,内部类就像一道墙,分割了内外,所有的逻辑被封装在迭代器实现类中
不需要影响容器自身的设计实现,所以也是符合单一职责原则的。