说起迭代器(Iterator),相信你并不会陌生,因为我们几乎每天都在使用JDK中自带的各种迭代器。那么,这些迭代器是如何构建出来的呢?就需要用到了今天内容要介绍的迭代器设计模式。在日常开发过程中,我们可能很少会自己去实现一个迭代器,但掌握迭代器设计模式对于我们学习一些开源框架的源码还是很有帮助的,因为在像Mybatis等主流开发框架中都用到了迭代器模式。
迭代器设计模式的概念和简单示例
在对迭代器模式的应用场景和方式进行展开之前,让我们先来对它的基本结构做一些展开。迭代器是这样一种结构:它提供一种方法,可以顺序访问聚合对象中的各个元素,但又不暴露该对象的内部表示。
想要构建这样一个迭代器,我们就可以引入迭代器设计模式。迭代器模式的基本结构如下图所示。
上图中的Aggregate相当于是一个容器,致力于提供符合Iterator实现的数据格式。当我们访问容器时,则是使用Iterator提供的数据遍历方法进行数据访问,这样处理容器数据的逻辑就和容器本身的实现了解耦,因为我们只需要使用Iterator接口就行了,完全不用关心容器怎么实现、底层数据如何访问之类的问题。而且更换容器的时候也不需要修改数据处理逻辑。
明白了迭代器模式的基本结构,接下来我们来给出对应的案例代码。首当其冲的,我们需要实现一个Iterator接口,如下所示。
public interface Iterator<T> {
//是否存在下一个元素
boolean hasNext();
//获取下一个元素
T next();
}
注意到这里使用的泛型结构,意味着这个迭代器接口可以应用到各种数据结构上。而这里的hasNext和next方法分别用来判断迭代器中是否存在下一个元素,以及下一个元素具体是什么。
然后,我们可以创建一个代表元素的数据结构,例如像这样的Item类。
public class Item {
private ItemType type;
private final String name;
public Item(ItemType type, String name) {
this.setType(type);
this.name = name;
}
…
}
注意到这里包含了两个参数,一个是ItemType枚举,代表Item的类型,另一个则指定Item的名称。
如果我们把Item看做是一个个宝物,那么我们就可以构建一个宝箱(TreasureChest)类,
public class TreasureChest {
private final List<Item> items;
public TreasureChest() {
items = List.of(
new Item(ItemType.POTION, "勇气药剂"),
new Item(ItemType.RING, "阴影之环"),
new Item(ItemType.POTION, "智慧药剂"),
new Item(ItemType.WEAPON, "银色之剑"),
new Item(ItemType.POTION, "腐蚀药剂"),
new Item(ItemType.RING, "盔甲之环"),
new Item(ItemType.WEAPON, "毒之匕首"));
}
public Iterator<Item> iterator(ItemType itemType) {
return new TreasureChestItemIterator(this, itemType);
}
public List<Item> getItems() {
return new ArrayList<>(items);
}
}
结合迭代器模式的基本结构,这个TreasureChest类相当于就是代表容器的Aggregate类,该类依赖于Iterator接口,同时又负责创建一个迭代器组件TreasureChestItemIterator。TreasureChestItemIterator类如下所示。
public class TreasureChestItemIterator implements Iterator<Item> {
//当前项索引
private int idx;
private final TreasureChest chest;
private final ItemType type;
public TreasureChestItemIterator(TreasureChest chest, ItemType type) {
this.chest = chest;
this.type = type;
this.idx = -1;
}
@Override
public boolean hasNext() {
return findNextIdx() != -1;
}
@Override
public Item next() {
idx = findNextIdx();
if (idx != -1) {
return chest.getItems().get(idx);
}
return null;
}
//寻找下一个Idx
private int findNextIdx() {
var items = chest.getItems();
var tempIdx = idx;
while (true) {
tempIdx++;
if (tempIdx >= items.size()) {
tempIdx = -1;
break;
}
if (type.equals(ItemType.ANY) || items.get(tempIdx).getType().equals(type)) {
break;
}
}
return tempIdx;
}
}
TreasureChestItemIterator的实现主要就是基于当前项索引对Item进行动态遍历和判断。
案例的最后,我们可以构建一段测试代码完成对TreasureChest和TreasureChestItemIterator功能的验证,如下所示。
private static final TreasureChest TREASURE_CHEST = new TreasureChest();
var itemIterator = TREASURE_CHEST.iterator(ItemType.RING);
while (itemIterator.hasNext()) {
LOGGER.info(itemIterator.next().toString());
}
执行这段代码,不难想象我们可以得到如下所示的结果。
阴影之环
盔甲之环
显然,我们获取了对应类型的Item数据,而这个过程对于测试代码而言是完全解耦的,我们不需要知道迭代器内部的运行原理,而只需要关注所返回的结果。
迭代器设计模式在Mybatis中的应用
介绍完迭代器模式的基本概念和代码示例,我们进一步来看看它是如何在主流开源框架中进行应用的。在Mybatis中,针对SQL中配置项语句的解析,专门设计并实现了一套迭代器组件。
Mybatis中存在两个类,通过了对迭代器模式的具体实现,分别是PropertyTokenizer和CursorIterator。我们先来看PropertyTokenizer的实现方法。
PropertyTokenizer
在Mybatis中,存在一个非常常用的工具类PropertyTokenizer,该类主要用于解析诸如“order[0].address.contactinfo.name”类型的属性表达式,在这个例子中,我们可以看到系统是在处理订单实体的地址信息,Mybatis支持使用这种形式的表达式来获取最终的“name”属性。我们可以想象一下,当我们想要解析“order[0].address.contactinfo.name”字符串时,我们势必需要先对其进行分段处理以分别获取各个层级的对象属性名称,如果遇到“[]”符号表示说明要处理的是一个对象数组。这种分层级的处理方式可以认为是一种迭代处理方式,作为迭代器模式的实现,PropertyTokenizer对这种处理方式提供了支持,该类代码如下所示。
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name;
private final String indexedName;
private String index;
private final String children;
public PropertyTokenizer(String fullname) {
int delim = fullname.indexOf('.');
if (delim > -1) {
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
name = fullname;
children = null;
}
indexedName = name;
delim = name.indexOf('[');
if (delim > -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
…
@Override
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
}
}
针对“order[0].address.contactinfo.name”字符串,当启动解析时,PropertyTokenizer类的name字段指的就是“order”,indexedName字段指的就是“order[0]”,index字段指的就是“0”,而children字段指的就是“address.contactinfo.name”。在构造函数中,当对传入的字符串进行处理时,通过“.”分隔符将其分作两部分。然后在对获取的name字段提取“[”,把中括号里的数字给解析出来,如果name段子你中包含“[]”的话,分别获取index字段并更新name字段。
通过构造函数对输入字符串进行处理之后,PropertyTokenizer的next()方法非常简单,直接再通过children字段再来创建一个新的PropertyTokenizer实例即可。而经常使用的hasNext()方法实现也很简单,就是判断children属性是否为空。
我们再来看PropertyTokenizer类的使用方法,我们在org.apache.ibatis.reflection包的MetaObject类中找到了它的一种常见使用方法,代码如下所示。
public Object getValue(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return null;
} else {
return metaValue.getValue(prop.getChildren());
}
} else {
return objectWrapper.get(prop);
}
}
这里可以明显看到通过PropertyTokenizer 的prop.hasNext()方法进行递归调用的代码处理流程。
CursorIterator
其实,迭代器模式有时还被称为是游标(Cursor)模式,所以通常可以使用该模式构建一个基于游标机制的组件。我们数据库访问领域中恰恰就有一个游标的概念,当查询数据库返回大量的数据项时可以使用游标Cursor,利用其中的迭代器可以懒加载数据,避免因为一次性加载所有数据导致内存奔溃。而Mybatis又是一个数据库访问框架,那么在这个框架中是否存在一个基于迭代器模式的游标组件呢?答案是肯定的,让我们来看一下。
Mybatis提供了Cursor接口用于表示游标操作,该接口位于org.apache.ibatis.cursor包中,定义如下所示。
public interface Cursor<T> extends Closeable, Iterable<T> {
boolean isOpen();
boolean isConsumed();
int getCurrentIndex();
}
同时,Mybatis为Cursor接口提供了一个默认实现类DefaultCursor,核心代码如下。
public class DefaultCursor<T> implements Cursor<T> {
private final CursorIterator cursorIterator = new CursorIterator();
@Override
public boolean isOpen() {
return status == CursorStatus.OPEN;
}
@Override
public boolean isConsumed() {
return status == CursorStatus.CONSUMED;
}
@Override
public int getCurrentIndex() {
return rowBounds.getOffset() + cursorIterator.iteratorIndex;
}
// 省略其他方法
}
我们看到这里引用了CursorIterator,从命名上就可以看出这是一个迭代器,其代码如下所示。
private class CursorIterator implements Iterator<T> {
T object;
int iteratorIndex = -1;
@Override
public boolean hasNext() {
if (object == null) {
object = fetchNextUsingRowBound();
}
return object != null;
}
@Override
public T next() {
// Fill next with object fetched from hasNext()
T next = object;
if (next == null) {
next = fetchNextUsingRowBound();
}
if (next != null) {
object = null;
iteratorIndex++;
return next;
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException("Cannot remove element from Cursor");
}
}
上述游标迭代器CursorIterator实现了java.util.Iterator 迭代器接口,这里的迭代器模式实现方法实际上跟 ArrayList 中的迭代器几乎一样。
对于系统中具有对元素进行迭代访问的应用场景而言,迭代器设计模式能够帮助我们构建优雅的迭代操作。现实中有数据访问方式都与迭代器相关,通过迭代器模式可以构建出灵活而高效的迭代器组件。在今天的内容中,我们通过详细的示例代码对这一设计模式的基本结构进行了展开,并分析了它在Mybatis框架中的两处具有代表性的应用场景以及实现方式。