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

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

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

相关文章

查找效率满分的算法—— “二分查找” 算法 (Java版)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

removeAttribute和removeAttributeNode有什么区别(代码举例说明)

removeAttribute 和 removeAttributeNode 都是用于从 HTML 元素中移除属性的 DOM 方法&#xff0c;但它们在用法和接受的参数上有一些区别。 removeAttribute removeAttribute 是一个元素&#xff08;Element&#xff09;对象的方法&#xff0c;它接受一个字符串参数&#xf…

深入了解Nginx(一):Nginx核心原理

一、Nginx核心原理 本节为大家介绍Nginx的核心原理,包含Reactor模型、Nginx的模块化设计、Nginx的请求处理阶段. &#xff08;本文源自微博客,且已获得授权&#xff09; 1.1、Reactor模型 Nginx对高并发IO的处理使用了Reactor事件驱动模型。Reactor模型的基本组件包含时间收集…

华为OBS命令行简单使用

华为OBS&#xff08;Object Storage Service&#xff09;是一种云存储服务&#xff0c;提供了高可靠、高性能、安全的数据存储能力。通过使用OBS的命令行工具obsutil&#xff0c;用户可以方便地进行文件上传、下载、删除等操作&#xff0c;而无需依赖图形界面。下面&#xff0c…

使用xsd验证xml格式的正确性

1.1 基础知识介绍 XML简介&#xff1a;XML是可扩展标记语言&#xff08;eXtensible Markup Language&#xff09;的缩写&#xff0c;它是一种数据表示格式&#xff0c;可以描述非常复杂的数据结构&#xff0c;常用于传输和存储数据。xml文件、xml消息。XSD简介&#xff1a;是X…

oracle 表同一列只取最新一条数据写法

select * from (select t.*,row_number() over(partition by 去重列名 order by 排序列名 desc) as rnfrom 表名)where rn1 1.row_number() over(....): 为每条数据分配一个行号,1.2.3....这样的 2.partition by : 以某列作为分组&#xff0c;每个分组行号从1开始&#xf…

ComputerLab实例2.0(继承)

要求&#xff1a; Write a computer program that could be used to track users activities. Lab NumberComputer Station Numbers11-321-431-541-6 ➢ You run four computer labs. Each lab contains computer stations that are numbered as the above table. ➢ There…

LabVIEW和ZigBee无线温湿度监测

LabVIEW和ZigBee无线温湿度监测 随着物联网技术的迅速发展&#xff0c;温湿度数据的远程无线监测在农业大棚、仓库和其他需环境控制的场所变得日益重要。开发了一种基于LabVIEW和ZigBee技术的多区域无线温湿度监测系统。系统通过DHT11传感器收集温湿度数据&#xff0c;利用Zig…

uniapp-自定义navigationBar

封装导航栏自定义组件 创建 nav-bar.vue <script setup>import {onReady} from dcloudio/uni-appimport {ref} from vue;const propsdefineProps([navBackgroundColor])const statusBarHeight ref()const navHeight ref()onReady(() > {uni.getSystemInfo({success…

图生代码,从Hello Onion 代码开始

从Hello Onion 代码开始 1&#xff0c;从代码开始 原生语言采用java 作为载体。通过注解方式实现“UI可视化元素"与代码bean之间的映射. 转换示例 2&#xff0c;运行解析原理 在执行JAVA代码期间&#xff0c;通过读取注解信息&#xff0c;转换为前端的JSON交由前端JS框…

NB49 牛群的秘密通信

描述 在一个远离人类的世界中&#xff0c;有一群牛正在进行秘密通信。它们使用一种特殊的括号组合作为加密通信的形式。每一组加密信息均包括以下字符&#xff1a;(,{,[,),},]。 加密信息需要满足以下有效性规则&#xff1a; 每个左括号必须使用相同类型的右括号闭合。左括号…

c++设计模式-->访问者模式

#include <iostream> #include <string> #include <memory> using namespace std;class AbstractMember; // 前向声明// 行为基类 class AbstractAction { public:virtual void maleDoing(AbstractMember* member) 0;virtual void femaleDoing(AbstractMemb…

OrangePiKunPengPro | 开发板学习与使用

OrangePi KunPengPro | 开发板学习与使用 时间:2024年5月23日20:51:12 文章目录 `OrangePi KunPengPro` | 开发板学习与使用1.参考2.资料2.使用2-1.通过串口登录系统2-2.通过SSH登录系统2-3.安装交叉编译工具链2-4.复制文件到设备1.参考 1.OrangePi Kunpeng Pro Orange Pi官网…

c语言指针入门(二)

今天学习了指针的两个常用场景&#xff0c;在此记录&#xff0c;以便后续查看。 场景1&#xff1a;传数组 在c语言中&#xff0c;我们在定义函数的时候是没有办法直接传一个数组进去的&#xff0c;为了解决这个问题&#xff0c;我们一般将数组的名称当作一个指针参数传入到函数…

mysql主从复制的步骤和使用到的操作命令有哪些?

步骤&#xff1a; 配置主服务器&#xff08;Master&#xff09;&#xff1a; 启用二进制日志记录&#xff08;binary logging&#xff09;。配置主服务器的唯一标识&#xff08;server-id&#xff09;。创建用于复制的专用复制账户。 配置从服务器&#xff08;Slave&#xff0…

安装Pnetcdf顺便升级autoconf与automake

Netcdf NetCDF&#xff08;Network Common Data Form&#xff09;是一种用于存储科学数据的文件格式和软件库。它是一种自描述、可移植且可扩展的数据格式&#xff0c;广泛应用于气象学、海洋学、地球科学和其他领域的科学研究。 NetCDF文件以二进制形式存储&#xff0c;结构…

Qt | QGridLayout 类(网格布局)

01、上节回顾 Qt | QBoxLayout 及其子类(盒式布局)02、QGridLayout 简介 1、网格布局原理(见下图): 基本原理是把窗口划分为若干个单元格,每个子部件被放置于一个或多个单元格之中,各 单元格的大小可由拉伸因子和一行或列中单元格的数量来确定,若子部件的大小(由 sizeH…

Vue从入门到实战 Day08~Day10

智慧商城项目 1. 项目演示 目标&#xff1a;查看项目效果&#xff0c;明确功能模块 -> 完整的电商购物流程 2. 项目收获 目标&#xff1a;明确做完本项目&#xff0c;能够收获哪些内容 3. 创建项目 目标&#xff1a;基于VueCli自定义创建项目架子 4. 调整初始化目录 目…

网络安全之BGP详解

BGP&#xff1b;边界网关协议 使用范围&#xff1b;BGP范围&#xff0c;在AS之间使用的协议。 协议的特点&#xff08;算法&#xff09;&#xff1a;路径矢量型&#xff0c;没有算法。 协议是否传递网络掩码&#xff1a;传递网络掩码&#xff0c;支持VLSM&#xff0c;CIDR …

【15】编写shell-安装mysql

说明: 1、请注意mysql版本的压缩包格式 2、请注意挂载data盘 3、请注意部署包和shell脚本放在同一个文件夹 4、实现shell脚本自动化部署mysql5.7.40版本 # !/bin/bash#****************************************************** # Author : 秋天枫叶35 # Last modified …