前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >一文带你彻底搞懂迭代器设计模式

一文带你彻底搞懂迭代器设计模式

原创
作者头像
写bug的高哈哈
发布2025-02-05 14:33:47
发布2025-02-05 14:33:47
800
举报

说起迭代器(Iterator),相信你并不陌生,因为我们几乎每天都在使用 JDK 中自带的各种迭代器。那么,这些迭代器是如何构建出来的呢?这就用到了今天我们要介绍的迭代器设计模式。在日常开发过程中,我们可能很少会自己去实现一个迭代器,但掌握迭代器设计模式对我们学习一些开源框架的源码还是很有帮助的,因为在像 MyBatis 等主流开发框架中都用到了迭代器模式。

迭代器设计模式的概念和简单示例

在学习迭代器模式的应用场景和方式之前,让我们先来对它的基本结构做一些展开。迭代器是这样一种结构:它提供一种方法,可以顺序访问聚合对象中的各个元素,但又不暴露该对象的内部表示。

图 1 迭代器的基本结构图
图 1 迭代器的基本结构图

想要构建这样一个迭代器,我们就可以引入迭代器设计模式。我们可以先看一下迭代器模式的基本结构。

图 2 迭代器模式的基本结构图
图 2 迭代器模式的基本结构图

图片中的 Aggregate 相当于一个容器,致力于提供符合 Iterator 接口定义的数据格式。当我们访问容器时,则是使用 ConcreteIterator 提供的数据遍历方法进行数据访问,这样处理容器数据的逻辑,就能让数据逻辑和容器本身实现解耦。因为我们只需要使用 Iterator 接口就行了,完全不用关心容器怎么实现、底层数据如何访问之类的问题,而且更换容器的时候也不需要修改数据处理逻辑。

明白了迭代器模式的基本结构,接下来我们给出对应的案例代码。首先,我们需要实现一个 Iterator 接口。

代码语言:java
复制
public interface Iterator<T> {
	//是否存在下一个元素
    boolean hasNext();
	//获取下一个元素
    T next();
}

注意这里我们使用的是泛型结构,意味着这个迭代器接口可以应用到各种数据结构上。而这里的 hasNext 和 next 方法分别用来判断迭代器中是否存在下一个元素,以及下一个元素具体是什么。

然后,我们可以创建一个代表元素的数据结构,例如像这样的 Item 类。

代码语言:java
复制
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)类。

代码语言:java
复制
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,这个类实现了 Iterator 接口,如下所示:

代码语言:java
复制
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 功能的验证,如下所示:

代码语言:java
复制
private static final TreasureChest TREASURE_CHEST = new TreasureChest();
var itemIterator = TREASURE_CHEST.iterator(ItemType.RING);
while (itemIterator.hasNext()) {
      LOGGER.info(itemIterator.next().toString());
}

执行这段代码,不难想象我们可以得到这样的结果:

代码语言:java
复制
阴影之环
盔甲之环

显然,我们获取了对应类型的 Item 数据,而这个过程对于测试代码而言是完全解耦的,我们不需要知道迭代器内部的运行原理,而只需要关注返回的结果。

迭代器设计模式在 MyBatis 中的应用

介绍完迭代器模式的基本概念和代码示例,我们进一步来看看它是如何在主流开源框架中应用的。在 MyBatis 中,针对 SQL 中配置项语句的解析,专门设计并实现了一套迭代器组件。

MyBatis 中存在两个类,提供了对迭代器模式的具体实现,分别是 PropertyTokenizer 和 CursorIterator。我们先来看 PropertyTokenizer 的实现方法。

PropertyTokenizer

PropertyTokenizer,是一个非常常用的工具类,这个类主要用来解析像“order[0].address.contactinfo.name”这种类型的属性表达式。在这个例子中,我们可以看到系统在处理订单实体的地址信息,MyBatis 支持使用这种形式的表达式来获取最终的“name”属性。可以想象一下,当我们想要解析“order[0].address.contactinfo.name”字符串时,我们势必需要先对其进行分段处理,来分别获取各个层级的对象属性名称,如果遇到“[]”符号,说明要处理的是一个对象数组。这种分层级的处理方式本质上就是一种迭代处理方式。作为迭代器模式的实现,PropertyTokenizer 对这种处理方式提供了支持,代码如下所示:

代码语言:java
复制
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 字段中提取“[”,把中括号里的数字给解析出来,如果字段中包含“[]”的话,分别获取 index 字段并更新 name 字段。

通过构造函数对输入字符串进行处理之后,PropertyTokenizer 的 next 方法就变得非常简单了,直接通过 children 字段再来创建一个新的 PropertyTokenizer 实例即可。而经常使用的 hasNext 方法实现也很简单,就是判断 children 属性是否为空。

我们再来看 PropertyTokenizer 类的使用方式,我们在 org.apache.ibatis.reflection 包的 MetaObject 类中找到了它的一种使用场景,代码如下所示:

代码语言:java
复制
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)模式,所以通常可以使用这个模式构建一个基于游标机制的组件。数据库访问领域中恰好就有一个游标的概念。当查询数据库返回大量数据项时可以使用游标,利用其中的迭代器实现对数据的懒加载,避免因为一次性加载所有数据导致内存崩溃。而 MyBatis 又是一个数据库访问框架,那么在这个框架中是否存在一个基于迭代器模式的游标组件呢?答案是肯定的,让我们来看一下。

MyBatis 提供了 Cursor 接口用于表示游标操作,这个接口位于 org.apache.ibatis.cursor 包中,定义如下所示:

代码语言:java
复制
public interface Cursor<T> extends Closeable, Iterable<T> {
  boolean isOpen();
  boolean isConsumed();
  int getCurrentIndex();
}

同时,MyBatis 为 Cursor 接口提供了一个默认实现类 DefaultCursor,核心代码如下:

代码语言:java
复制
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,从命名上就可以看出这是一个迭代器,其代码如下所示:

代码语言:java
复制
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 框架中的两处具有代表性的应用场景以及实现方式。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 迭代器设计模式的概念和简单示例
  • 迭代器设计模式在 MyBatis 中的应用
    • PropertyTokenizer
    • CursorIterator
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档