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

news2024/11/20 3:18:44

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

刷题之将有序数组转换成二叉搜索树(leetcode)

将有序数组转换成二叉搜索树 正常递归&#xff0c;中序遍历 递归经常会把自己绕晕&#xff0c;还是得画图分析 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(null…

【LeetCode 随笔】面试经典 150 题【中等+困难】持续更新中。。。

文章目录 380.【中等】O(1) 时间插入、删除和获取随机元素238.【中等】除自身以外数组的乘积134.【中等】 加油站135.【困难】分发糖果42.【困难】接雨水 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面…

【Docker实操】启动redis服务

一、步骤 1、获取redis镜像 执行获取redis镜像命令&#xff1a;docker pull redis。打印镜像清单&#xff0c;可以看到新拉到的redis镜像。 2、创建redis.conf配置文件 linux主机 mkdir -p /root/www/redis/conf touch /root/www/redis/conf/redis.conf cat << EOF &…

vue中数据已经改变了,但是table里面内容没更新渲染!

解决方案&#xff1a; 给table或者el-table标签上添加一个动态key值&#xff0c;只要数据发生改变&#xff0c;key值变动一下即可 标签上&#xff1a; :key“timeStamp” 初始data&#xff1a;timeStamp:0, 更新数据&#xff1a;this.timeStamp 这样每次更新数据&#xff…

网络的基础理解

文章目录 网络的基础认识 网络协议协议分层OSI七层模型TCP/IP 五层/四层 模型 网络的基础认识 先来看下面几个问题 什么是网络&#xff1f; 网络就是有许多台设备包括计算机单不仅限于计算机&#xff0c;这些设备通过相互通信所组成起来系统&#xff0c;我们称之为网络所以如…

通过 Spring 操作 Redis

要想通过 Java 操作 redis&#xff0c;首先要连接上 redis 服务器&#xff0c;推荐看通过 Java 操作 redis -- 连接 redis 创建项⽬ 勾选 NoSQL 中的 Spring Data Redis 当然, 把 Web 中的 Spring Web 也勾选⼀下.⽅便写接进⾏后续测试. 配置 redis 服务地址 在 application.…

CF451E: Devu and Flowers(容斥原理 + 考虑反面 + golang组合模版)

题目截图 题目翻译 题目分析 正难则反&#xff0c;考虑所有不符合的例子 由于n很小&#xff0c;所以可以状态压缩二进制遍历完全部不符合例子的组合 对于不符合的例子&#xff0c;假设其中第i个不符合&#xff0c;那么就消耗掉fi 1个球 以此类推&#xff0c;减剩下s2个球 这时…

盘点28个免费域名申请大全

盘点28个免费域名申请大全 免费域名推荐学习使用&#xff0c;免费就意味着没任何保障。 名称稳定时间支持解析模式后缀格式说明地址EU.org28 年NS.eu.org/. 国家简写.eu.org需要审核&#xff0c;稳定性高&#xff0c;限制少&#xff0c;国内访问有问题&#xff0c;可 CFeu.orgp…

关于「公 告」根据中华人民共和国法律,Bing 在中国内地暂停 “搜索自动建议” 功能 30 天

当我看见我们大家都这样我可放心了&#xff0c;我打开电脑搜索图片就发生了。 当我看见我们大家都这样我可放心了&#xff0c;坐等攻城狮修复。

前端vue用el-table如何实现表头内容过长换行处理,实现换行效果

前端vue用el-table如何实现表头内容过长换行处理&#xff0c;实现换行效果 这是效果图 有两种方法&#xff0c;一种简易版本&#xff0c;一种万能方法,都是el-table&#xff0c;先看文档 表头标题是可以自定义的 方法一 label的解释写在代码里面了&#xff0c;这里会自动形成换…

六零导航页 file.php 任意文件上传漏洞复现(CVE-2024-34982)

0x01 产品简介 LyLme Spage(六零导航页)是中国六零(LyLme)开源的一个导航页面。致力于简洁高效无广告的上网导航和搜索入口,支持后台添加链接、自定义搜索引擎,沉淀最具价值链接,全站无商业推广,简约而不简单。 0x02 漏洞概述 六零导航页 file.php接口处任意文件上传…

中银基金软件开发工程师春招群面记录

本文介绍2024届春招中&#xff0c;中国银行下属中银基金管理有限公司的软件开发工程师岗位1场面试的基本情况、提问问题等。 2024年04月投递了中国银行的共计4个部门或单位&#xff0c;包括中银基金管理有限公司的软件开发工程师岗位&#xff0c;暂时不清楚所在部门。目前完成了…

AWS安全性身份和合规性之Artifact

AWS Artifact是对您很重要的与合规性相关的信息的首选中央资源。AWS Artifact是一项服务&#xff0c;提供了一系列用于安全合规的文档、报告和资源&#xff0c;以帮助用户满足其合规性和监管要求。它允许按需访问来自AWS和在AWS Marketplace上销售产品的ISV的安全性和合规性报告…

I.MX6Ull交叉编译QT项目并用u盘拷贝到开发板下

1.安装交叉编译器 2.命令行交叉编译Qt项目 3.拷贝编译好的执行程序到开发板运行 下载到虚拟机 修改权限chom u x 名称 执行完后&#xff0c;变色 执行脚本文件&#xff0c;使能 source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi …

利用远控工具横向

一.横向移动介绍和方式 1.介绍 内网渗透的横向移动是指攻击者在成功进入内网后&#xff0c;通过利用内部系统的漏洞或者获取的合法访问权限&#xff0c;从一个受感染的系统向其他系统扩散或移动。这种横向移动的目的通常是为了获取更多的敏感信息、提升权限、扩大攻击面或者更…

将PCD点云投影到BEV平面得到图片

前言 点云数据作为一种丰富的三维空间信息表达方式&#xff0c;通常用于自动驾驶、机器人导航和三维建模等领域。然而&#xff0c;点云数据的直观性不如二维图像&#xff0c;这限制了它在一些需要快速视觉反馈的应用场景中的使用。本文将探讨如何将点云数据转换为二维图像&…

【传知代码】从零开始搭建图像去雾神经网络-论文复现

文章目录 概述原理介绍网络结构 核心逻辑迁移学习子网数据拟合子网 环境配置训练本次复现代码所用数据集测试本次复现代码所用的评价指标 结果展示在O-Haze数据集上的结果在I-Haze数据集上的结果 小结 本文涉及的源码可从从零开始搭建图像去雾神经网络该文章下方附件获取 本文复…

React中显示数据

SX 会让你把标签放到 JavaScript 中。而大括号会让你 “回到” JavaScript 中&#xff0c;这样你就可以从你的代码中嵌入一些变量并展示给用户。例如&#xff0c;这将显示 user.name&#xff1a; return (<h1>{user.name}</h1> ); 你还可以将 JSX 属性 “转义到 …

内网安全之搭建ADCS证书服务

在域控上安装ADCS服务时&#xff0c;默认会自动配置完LDAPS&#xff0c;如果不是在域控上安装ADCS服务&#xff0c;需要手动配置LDAPS 安装证书服务ADCS 打开服务器管理器——>添加角色和功能 选择“基于角色或基于功能的安装”选项&#xff0c;然后点击下一步 选择“从…

我的前端封装之路

最近有粉丝提问了我一个面试中遇到的问题&#xff0c;他说面试的时候&#xff0c;面试官问我&#xff1a;你在以前的项目中封装过组件吗&#xff1f;或者做过npm公共库吗&#xff1f;遇到过什么问题吗&#xff1f;当时自己突然觉得好像没什么可回答的啊&#xff0c;但面试结束想…