迭代子模式,又叫游标(Cursor)模式,是一种行为型设计模式。其思想是:利用迭代子顺序地访问一个聚集中的元素而不必暴露聚集的内部表象(internal representation)。
几个基本概念
- 聚集(Aggregate):多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。聚集依赖于聚集结构的抽象化,具有复杂化和多样性。数组就是最基本的聚集,也是其他的JAVA聚集对象的设计基础。JAVA聚集对象是实现了共同的
java.util.Collection
接口的对象,是JAVA语言对聚集概念的直接支持。 - 宽接口(Wide Inteface):如果一个类向外提供了一个或多个访问其内部成员的方法,那么这个类就称为宽接口。对于聚集,如果这个聚集类向外提供了直接访问其聚集元素的方法,那么这个聚集就是宽接口。
- 窄接口(Narrow Inteface):与宽接口相反,如果一个类没有向外提供访问其内部成员的方法,那么这个类就称为窄接口。对于聚集,如果这个聚集类没有向外提供访问其聚集元素的方法,那么这个聚集就是窄接口。
- 白箱聚集(White Box Aggregate):即宽接口聚集。
- 黑箱聚集(Black Box Aggregate):即窄接口聚集。
- 外禀迭代子(Extrinsic Iterator):由于聚集自己实现迭代逻辑,并向外部提供适当的接口,使得迭代子可以从外部控制聚集元素的迭代过程,这样的迭代子称为外禀迭代子。
- 内禀迭代子(Extrinsic Iterator):迭代子位于聚集内部,是聚集的成员内部类,可以自由访问聚集的元素,自行实现迭代功能并控制对聚集元素的迭代逻辑,这样的迭代子又叫做内禀迭代子。
迭代子模式的两种实现方式
迭代子模式的两种实现方式,分别是白箱聚集+外禀迭代子和黑箱聚集+内禀迭代子。
白箱聚集+外禀迭代子
一个白箱聚集向外界提供访问自己内部元素的接口(称作遍历方法或者Traversing Method),从而使外禀迭代子可以通过聚集的遍历方法实现迭代功能。由于迭代的逻辑是由聚集对象本身提供的,所以这样的外禀迭代子角色往往仅仅保持迭代的游标位置。如图:
(图片来源于网络)
具体代码实现:
聚集:
public interface Aggregate {
Iterator iterator(); // 声明一个获取迭代子的方法
}
public class ConcreteAggregate implements Aggregate {
private Object[] arr; // 实现聚集功能的底层数组
public ConcreteAggregate(Object[] arr) {
this.arr = arr;
}
// 返回聚集大小
public int size() {
return arr.length;
}
// 向外提供一个访问聚集元素的接口(返回指定下标的元素)
public Object getElement(int index) {
return index < arr.length ? arr[index] : null;
}
@Override
public Iterator iterator() {
return new ConcreteIterator(this);
}
}
迭代子:
public interface Iterator {
boolean hasNext(); // 是否有下一个元素
Object next(); // 游标后移一位,然后返回游标所指的元素
}
public class ConcreteIterator implements Iterator {
private ConcreteAggregate agg; // 持有一个具体聚集的引用
private int index; // 当前游标位置,起始于-1
private int size; // 持有的聚集的大小
public ConcreteIterator(ConcreteAggregate agg) {
this.agg = agg;
this.index = -1;
this.size = agg.size();
}
@Override
public boolean hasNext() {
return index + 1 < size;
}
@Override
public Object next() {
if (hasNext())
return agg.getElement(++index); // 通过聚集的遍历方法实现迭代功能
return null;
}
}
客户:
public class Client {
private Aggregate agg = new ConcreteAggregate(new Object[]{"AAA", "BBB", "CCC"});
public void method() {
// 遍历一个聚集
for (Iterator it = agg.iterator(); it.hasNext(); ) {
Object obj = it.next();
System.out.println(obj);
}
}
}
// 测试
class IteratorTest {
public static void main(String[] args) {
Client client = new Client();
client.method();
}
}
运行结果:
AAA
BBB
CCC
结构图:
也许有人会问:既然白箱聚集已经向外界提供了遍历方法,客户端已经可以自行进行迭代了,为什么还要应用迭代子模式,并创建一个迭代子对象进行迭代呢?
答案是:解耦,具体来说就是降低客户
与聚集
之间的耦合关系。
客户端当然可以自行进行迭代,不一定非得需要一个迭代子对象。但是,迭代子对象和迭代模式会将迭代过程抽象化,将作为迭代消费者的客户
与迭代负责人的迭代子
责任分隔开,使得两者可以独立的演化。在聚集对象的种类发生变化,或者迭代的方法发生改变时,迭代子作为一个中介层可以吸收变化的因素,从而避免修改客户端或者聚集本身。
此外,如果系统需要同时针对几个不同的聚集对象进行迭代,而这些聚集对象所提供的遍历方法有所不同时,具有同一迭代接口的不同迭代子对象处理具有不同遍历接口的聚集对象,使得系统可以使用一个统一的迭代接口进行所有的迭代。
黑箱聚集+内禀迭代子
这种实现方式的思路是:聚集对象为迭代子对象提供一个宽接口,而为其他对象提供一个窄接口,也就是将迭代子类设计成聚集类的内部成员类。如图:
(图片来源于网络)
具体代码实现:
// 抽象迭代子
public interface Iterator {
boolean hasNext();
Object next();
}
// 抽象聚集
public interface Aggregate {
Iterator iterator();
}
// 具体聚集
public class ConcreteAggregate implements Aggregate {
private Object[] arr;
public ConcreteAggregate(Object[] arr) {
this.arr = arr;
}
public int size() {
return arr.length;
}
@Override
public Iterator iterator() {
return new ConcreteIterator();
}
// 将具体迭代子设计成具体聚集的成员内部类
private class ConcreteIterator implements Iterator {
private int size;
private int index;
private ConcreteIterator() {
this.size = size();
this.index = -1;
}
@Override
public boolean hasNext() {
return index + 1 < size;
}
@Override
public Object next() {
if (hasNext())
return arr[++index]; // 直接访问聚集元素
return null;
}
}
}
客户和测试的代码和第一个例子是一样的,不再贴出。
运行结果:
AAA
BBB
CCC
结构图:
可以看到,聚集并没有向外提供访问自身元素的方法,客户不能直接遍历,但作为内部成员的具体迭代子则可以访问聚集元素,所以客户能具只能通过这个迭代子来遍历聚集。
一些相关的概念
主动迭代子和被动迭代子
主动迭代子指的是由客户端来控制迭代下一个元素的步骤,客户端会明显调用迭代子的next()
等迭代方法,在遍历过程中向前进行。前面的两个例子都是主动迭代子。
被动迭代子,指的是由迭代子自己来控制迭代下一个元素的步骤。因此,如果想要在迭代的过程中完成工作的话,客户端就需要把操作传递给迭代子,迭代子在迭代的时候会在每个元素上执行这个操作,类似于JAVA的回调机制。
JDK1.8在java.util.Iterator
接口内新增了一个默认方法,使我们很容易就可以实现被动迭代。源码如下:
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
这里提供一个小例子:
public class Client {
private List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
public void change() {
Iterator<Integer> it = list.iterator();
it.forEachRemaining(x -> {
x *= 2;
System.out.println(x);
});
}
}
// 测试
class ClientTest {
public static void main(String[] args) {
Client client = new Client();
client.change();
}
}
运行结果:
2
4
6
8
10
静态迭代子和动态迭代子
静态迭代子由聚集对象创建,并持有聚集对象的一份快照(snapshot),在产生后这个快照的内容就不再变化。客户端可以继续修改原聚集的内容,但是迭代子对象不会反映出聚集的新变化。
静态迭代子的好处是它的安全性和简易性。但是由于静态迭代子将原聚集复制了一份,因此它的短处是对时间和内存资源的消耗。
动态迭代子则与静态迭代子完全相反,在迭代子被产生之后,迭代子保持着对聚集元素的引用,因此,任何对原聚集内容的修改都会在迭代子对象上反映出来。
完整意义上的动态迭代子并不容易实现,实际操作中我们一般使用简化的动态迭代子。所谓简化的动态迭代子,就是当检测到聚集发现改变时,迭代子抛出一个异常来中止迭代。
关于静态迭代和动态迭代,又涉及到另外两个重要的概念,请往下看。
Fail-fast机制和Fail-safe机制
Fail-fast机制:在遍历一个集合时,当集合结构被修改,会抛出ConcurrentModificationException
。有以下两种情况:
- 单线程环境:
集合被创建后,在遍历它的过程中修改了结构。
注意:如果使用迭代器的remove()
方法,只有当下标越界的情况下才会抛出concurrentmodifyexception
,而调用集合的remove()
方法,无论是单线程和多线程情况下,都会出现异常。 - 多线程环境:
当一个线程在遍历这个集合,而另一个线程对这个集合的结构进行了修改。
注意,迭代器的fail-fast行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。Fail-fast迭代器会尽最大努力抛出ConcurrentModificationException
。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法(因为根本不可靠啊!)。迭代器的fail-fast行为应该仅用于检测 bug。
Fail-fast机制的实现:迭代器在遍历过程中是直接访问内部数据的,因此内部的数据在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个标记 “mode” ,当集合结构改变(添加删除或者修改),标记”mode”会被修改,而迭代器每次的hasNext()
和next()
方法都会检查该”mode”是否被改变,当检测到被修改时,抛出ConcurrentModificationException
。
请看下面java.util.ArrayList
迭代子部分的源码:
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount; // 这个就是内部标记“mode”
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification(); // 遍历的每一步前都会先检查标记是否被修改
/** 省略此处代码 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此处代码 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException(); // 当标记被修改,则抛出异常
}
}
Fail-safe机制:任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException
。
Fail-safe机制有两个明显的缺点:
- 需要复制集合,产生大量的冗余对象,开销巨大
- 无法保证读取的数据是最新的数据
JAVA为实现fail-safe机制提供了一套API,放在java.util.concurrent
包下。
Fail-fast和Fail-safe的比较如下表:
` | Fail-Fast | Fail-safe |
| Yes | No |
聚集快照(snapshot) | No | Yes |
占用内存 | 正常 | 大 |
例子 |
|
|