如何彻底搞懂迭代器(Iterator)设计模式?

说起迭代器(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框架中的两处具有代表性的应用场景以及实现方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/633896.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多尺度注意力机制突破性成果!低成本、高性能兼备

与传统的注意力机制相比&#xff0c;多尺度注意力机制引入了多个尺度的注意力权重&#xff0c;让模型能够更好地理解和处理复杂数据。 这种机制通过在不同尺度上捕捉输入数据的特征&#xff0c;让模型同时关注局部细节和全局结构&#xff0c;以提高对细节和上下文信息的理解&a…

开源大模型与闭源大模型:技术哲学的较量

目录 前言一、 开源大模型的优势1. 社区支持与合作1.1 全球协作网络1.2 快速迭代与创新1.3 共享最佳实践 2. 透明性与可信赖性2.1 审计与验证2.2 减少偏见与错误2.3 安全性提升 3. 低成本与易访问性3.1 降低研发成本3.2 易于定制化3.3 教育资源丰富 4. 促进标准化5. 推动技术进…

3d选择模型后不能旋转什么原因?怎么解决?---模大狮模型网

在3D建模和渲染的过程中&#xff0c;旋转模型是常见的操作。然而&#xff0c;有时在选择了模型后&#xff0c;却发现无法进行旋转&#xff0c;这可能会让许多用户感到困扰。本文将探讨3D选择模型后不能旋转的可能原因&#xff0c;并提供相应的解决方法。 一、3D选择模型后不能旋…

Zynq-Linux移植学习笔记之68- 国产ZYNQ添加用户自定义版本信息

1、背景介绍 在使用复旦微zynq时&#xff0c;有时候虽然针对uboot源码进行了改动&#xff0c;但由于uboot基线版本只有一个&#xff08;2018-07-fmsh&#xff09;&#xff0c;导致无法区分版本信息&#xff0c;虽然可以通过编译时间来区分&#xff0c;但没有版本号直观。内核也…

快速搭建 WordPress 外贸电商网站指南

本指南全面解析了在 Hostinger 平台上部署 WordPress 外贸电商网站的详细步骤&#xff0c;涵盖托管方案选择、WordPress 一键安装、主题挑选与演示数据导入、主题个性化定制、SEO插件插件 AIOSEO 安装、通过 GTranslate 实现多语言自动翻译、地区访问控制插件&#xff0c;助力用…

高中数学:平面向量-数量积(向量与向量的乘积)与投影

一、引题 物理上的力做功 二、数量积与投影 1、数量积 θ的范围是[0,π] 2、投影 向量的投影&#xff0c;依然是一个向量&#xff01; 3、运算法则 易错点&#xff1a; 4、重要性质 这里对性质(2)要注意一下&#xff1a;如果 a → \mathop{a}\limits ^{\rightarrow…

30.包名的修改和新建后端模块

权限和第三方登录确实令人头疼,我们来学一点简单一点的。 另外,如果各位有属于自己的域名和ICP/IP备案,布置一个作业,自行实现第三方QQ登录。 我们所说的包名修改,是一次性修改ruoyi的全部包名,因为发现很多人有这样的需求,下载别人的代码,想要改成自己公司的包名,结…

当代家庭教育杂志社《当代家庭教育》杂志社24年第6期目录

家庭教育资讯 《家庭教育蓝皮书2024:中国家庭养育环境报告》出炉 4 2024年4月至7月北京市将开展“双减”专项行动 5 小学生玩“烟卡”到底该不该禁&#xff1f; 5 家庭教育理论探索 新时代家长家庭教育素养&#xff1a;意涵、关键要素及其培育 周起煌; 6-10 …

海外仓WMS系统多少钱?家庭海外仓怎么选合适的系统

作为海外仓管理的核心工具&#xff0c;WMS系统能够帮助企业实现仓库的可视化管理&#xff0c;流程自动化以及决策的数据化支持&#xff0c;进而提升海外仓的整体竞争力。 然而&#xff0c;许多海外仓企业在选择wms系统的时候&#xff0c;往往对价格的疑虑比较大&#xff0c;不…

微服务远程调用 RestTemplate

Spring给我们提供了一个RestTemplate的API&#xff0c;可以方便的实现Http请求的发送。 同步客户端执行HTTP请求&#xff0c;在底层HTTP客户端库(如JDK HttpURLConnection、Apache HttpComponents等)上公开一个简单的模板方法API。RestTemplate通过HTTP方法为常见场景提供了模…

Xinstall全渠道统计服务,洞悉App推广效果

在当今数字化时代&#xff0c;App已经成为企业和个人进行业务推广和服务提供的重要渠道。然而&#xff0c;随着App市场的日益饱和&#xff0c;如何有效地推广和运营App成为了众多广告主和开发者面临的难题。而App渠道统计作为衡量推广效果、优化运营策略的重要手段&#xff0c;…

大数据运维学习笔记之Ambari——筑梦之路

原则&#xff1a;分布式存储和分布式计算分开 今天就到这里啦。

Github上传时报错The file path is empty的解决办法

问题截图 文件夹明明不是空的&#xff0c;却怎么都上传不上去。 解决方案&#xff1a; 打开隐藏文件的开关&#xff0c;删除原作者的.git文件 如图所示&#xff1a; 上传成功&#xff01;

JS片段:如何将文本复制到剪贴板

在构建网站时&#xff0c;一个非常普遍的需求是能够通过单击按钮将文本复制到剪贴板。在现代浏览器中&#xff0c;使用navigator.clipboardAPI 访问系统剪切板&#xff0c;利用clipboard.writeText复制到剪贴板&#xff0c;再通过clipboard.readText获取剪贴板内容即可实现。 …

APH-Archives of Public Health

文章目录 一、期刊简介二、征稿信息三、期刊表现四、投稿须知五、投稿咨询 一、期刊简介 Archives of Public Health是一份范围广泛的公共卫生杂志&#xff0c;致力于出版公共卫生领域所有可靠的科学。该杂志旨在更好地了解人群的健康。该杂志有助于公共卫生知识&#xff0c;加…

Debug-010-git stash的用法及使用场景

问题原因&#xff1a; 其实也不是最近&#xff0c;就是之前就碰到过这个问题&#xff0c;那就是我正在新分支开发新功能&#xff0c;开发程度还没有到可以commit的程度&#xff0c;我不想提交(因为有些功能没有完全实现&#xff0c;而且没有自测的话很容易有问题&#xff0c;提…

智能车竞赛指南:从零到一,驶向自动驾驶的未来

智能车竞赛指南&#xff1a;从零到一&#xff0c;驶向自动驾驶的未来 一、智能车竞赛概览1.1 竞赛介绍1.2 竞赛分类 二、智能车开发技术基础2.1 硬件平台2.2 软件开发 三、实战案例&#xff1a;循线小车开发3.1 系统架构3.2 代码示例 四、技术项目&#xff1a;基于ROS的视觉导航…

4月粽子行业线上市场销售数据分析

随着节日庆祝常态化&#xff0c;消费者对礼物消费的态度发生变化&#xff0c;这会影响粽子的消费模式和市场需求。再加上技术进步&#xff0c;如速冻粽子和真空粽子的推广&#xff0c;也极大地推动了粽子行业的发展&#xff0c;使得产品更易于保存和运输&#xff0c;从而满足了…

linux系统内存持续飙高的排查方法

目录 前言&#xff1a; 1、查看系统内存的占用情况 2、找出占用内存高的进程 3、解决方法 4、补充&#xff1a;如果物理内存使用完了&#xff0c;会发生的情况 前言&#xff1a; 如果一台服务器内存使用率持续处于一个高峰值&#xff0c;服务器可能会出现响应慢问题。例如s…

linux系统硬盘读写慢的排查方法

如果服务器硬盘读写慢&#xff0c;可能会导致处理性能降低&#xff0c;用户响应慢。因此及时排除故障至关重要。下面是硬盘读写慢的排查思路。 1、top命令查看硬盘是否繁忙。 2、找出占用硬盘带宽高的进程。 通过iotop命令进行查看&#xff0c;iotop命令是用于展示硬盘读写操作…