设计模式学习笔记 - 设计模式与范式 -行为型:11.迭代器模式(下):如何设计实现一个支持“快照”功能的Iterator

news/2024/5/2 4:14:01/文章来源:https://blog.csdn.net/chenjian723122704/article/details/137611572

概述

前两篇文章,学习了迭代器模式的原理、实现,并分析了在遍历集合的同时增删元素集合,产生不可预期结果的原因及应对策略。

本章,再来看这样一个问题: 如何实现一个支持 “快照” 功能的迭代器?

这个问题算是上一篇文章内容的延升思考,为的是帮助你加深对迭代器模式的理解。


问题描述

先介绍下问题的背景:如何实现一个支持 “快照” 功能的迭代器?

理解这个问题的关键是理解 “快照”。 所谓 “快照” ,指我们为容器创建迭代器时,相当于给容器拍了一张快照(Snapshot)。之后即便我们增删容器中的元素,快照中的元素并不会做相应的改动。而迭代器遍历的对象是快照而非容器,这样就避免了在使用迭代器遍历的过程中,增删元素导致的不可预期结果或者报错。

接下来,通过一个例子来解释下。具体代码如下所示,容器 list 中初始存储了 3、8、2 三个元素。尽管在创建迭代器 iter1 之后,容器删除了元素 3,只剩剩下 8、2,但是,通过 iter1 遍历的对象是快照,而非容器 list 本身。所以,遍历的结果仍然是 3、8、2。同理,iter2iter3 也是在各占的快照上遍历,输出的结果如注释所示。

List<Integer> list = new ArrayList<>();
list.add(3);
list.add(8);
list.add(2);Iterator<Integer> iter1 = list.iterator(); // snapshot: 3,8,2
list.remove(3); // list: 8,2;
Iterator<Integer> iter2 = list.iterator(); // snapshot: 8,2
list.remove(2); // list: 8;
Iterator<Integer> iter3 = list.iterator(); // snapshot: 8// 输出:3,8,2
while (iter1.hasNext()) {System.out.print(iter1.next() + " ");
}
System.out.println();// 输出:8,2
while (iter2.hasNext()) {System.out.print(iter2.next() + " ");
}
System.out.println();// 输出:8
while (iter3.hasNext()) {System.out.print(iter3.next() + " ");
}
System.out.println();

如果让你实现上面的功能?你会如何做?

下面是针对这个功能需求的骨架代码,其中包含 ArrayListSnapshotArrayIterator 两个类。对于这两个类,文章只定义了几个关键接口,你可以自行尝试完善下,然后再看下面的讲解。

public class ArrayList<E> implements List<E> {// 成员变量、私有属性...@Overridepublic void add(E e) {// ...}@Overridepublic void remove(E e) {// ...}@Overridepublic Iterator<E> iterator() {return null;}
}public class SnapshotArrayIterator<E> implements Iterator<E> {// 成员变量、私有属性...@Overridepublic boolean hasNext() {// ...}@Overridepublic E next() {// ...}}

解决方案一

先看最简单的一种解决办法。在迭代器类中定义一个成员变量 snapshot 来存储快照。每当创建迭代器的时候,都拷贝一份容器中的元素到快照中,后续的遍历操作都基于这个迭代自己持有的快照来进行。

public class SnapshotArrayIterator<E> implements Iterator<E> {private int cursor;private ArrayList<E> snapshot;public SnapshotArrayIterator(ArrayList<E> arrayList) {this.cursor = 0;this.snapshot = new ArrayList<>();this.snapshot.addAll(arrayList);}@Overridepublic boolean hasNext() {return cursor < snapshot.size();}@Overridepublic E next() {E currentItem = snapshot.get(cursor);cursor++;return currentItem;}
}

这种解决方式虽然简单,但代价也有点高。每次创建迭代器时,都要拷贝一份数据到快照中,会增加内存的消耗。如果一个容器同时有多个迭代器正在遍历元素,就会导致数据在内存中重复存储多分。不过,庆幸的是,Java 中的拷贝属于浅拷贝,也就是说,容器中的对象并非真的拷贝了多份,而只是拷贝了对象的引用而已。关于深拷贝、浅拷贝,我们在《创建型:7.原型模式》中有详细的讲解,你可以回头去看一下。

解决方案二

可以在容器中,为每个元素保存两份时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。当元素被加入到集合中时,将 addTimestamp 设置问当前时间,将 delTimestamp 设置为 Long.MAX_VALUE。当元素被删除时,我们将 delTimestamp 更新为当前时间,表示已被删除。

注意,这里只是标记删除,而非真正将它从容器中删除。

同时,每个迭代器也保存一份迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应地快照的创建时间错。当使用迭代器遍历容器时,只有满足 addTimestamp<snapshotTimestamp<delTimestamp 的元素,才是属于这个迭代器的快照。

  • 如果元素的 addTimestamp>snapshotTimestamp,说明元素在创建了迭代器之后才加入,不属于这个迭代器的快照;
  • 如果元素的 delTimestamp<snapshotTimestamp,说明元素在创建迭代器之前就被删除了,也不属于这个迭代器的快照。

这样就在不拷贝容器的情况下,在容器身上借助时间戳实现了快照的功能。具体的代码实现如下所示。注意,这里没有考虑 ArrayList 的扩容问题,感兴趣的话,可以自己完善一下。

public class ArrayList<E> implements List<E> {private static final int DEFAULT_CAPACITY = 10;private int actualSize; //不包含标记删除元素private int totalSize; // 包含标记删除元素private Object[] elements;private long[] addTimestamps;private long[] delTimestamps;public ArrayList() {this.elements = new Object[DEFAULT_CAPACITY];this.addTimestamps = new long[DEFAULT_CAPACITY];this.delTimestamps = new long[DEFAULT_CAPACITY];this.actualSize = 0;this.totalSize = 0;}@Overridepublic void add(E e) {elements[totalSize] = e;addTimestamps[totalSize] = System.currentTimeMillis();delTimestamps[totalSize] = Long.MAX_VALUE;totalSize++;actualSize++;}@Overridepublic void remove(E e) {for (int i = 0; i < totalSize; i++) {if (elements[i].equals(e)) {delTimestamps[i] = System.currentTimeMillis();actualSize--;}}}public int actualSize() {return actualSize;}public int totalSize() {return totalSize;}@Overridepublic E get(int i) {if (i > totalSize) {throw new IndexOutOfBoundsException();}return (E)elements[i];}public long getAddTimestamp(int i) {if (i > totalSize) {throw new IndexOutOfBoundsException();}return addTimestamps[i];}public long getDelTimestamp(int i) {if (i > totalSize) {throw new IndexOutOfBoundsException();}return delTimestamps[i];}
}public class SnapshotArrayIterator<E> implements Iterator<E> {private long snapshotTimestamp;private int cursorInAll; // 在整个容器中的下标,而非快照中的下标private int leftCount; // 快照中还有几个元素未被遍历private ArrayList<E> arrayList;public SnapshotArrayIterator(ArrayList<E> arrayList) {this.snapshotTimestamp = System.currentTimeMillis();this.cursorInAll = 0;this.leftCount = arrayList.actualSize();this.arrayList.addAll(arrayList);justNext(); // 先跳到这个迭代器快照的第一个元素}@Overridepublic boolean hasNext() {return this.leftCount >= 0;}@Overridepublic E next() {E currentItem = arrayList.get(cursorInAll);justNext();return currentItem;}private void justNext() {while (cursorInAll < arrayList.totalSize()) {long addTimestamp = arrayList.getAddTimestamp(cursorInAll);long delTimestamp = arrayList.getDelTimestamp(cursorInAll);if (snapshotTimestamp > addTimestamp && snapshotTimestamp < delTimestamp) {leftCount--;break;}cursorInAll++;}}
}

实际上,上面的解决方案相当于解决了一个问题,又引入另一个问题。 ArrayList 底层依赖数组这种数据结构,原本可以支持快速地随机查询,在 O(1) 时间复杂内获取下标为 i 的元素,但现在,删除数据并非真正的删除,只是通过时间戳来标记,这就导致无法支持按照下标快速随机访问了。

那如何让容器既支持快照遍历,又支持随机访问呢?

解决的方法也不难。我们可以在 ArrayList 中存储两个数组。一个支持标记删除,用来实现快照遍历功能;一个不支持标记删除,用来支持随机访问(也就是将要删除的数据直接从数组中删除)。具体的代码就不实现了,感兴趣的话,可以自己实现下。

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

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

相关文章

5分钟了解清楚【osgb】格式的倾斜摄影数据metadata.xml有几种规范

数据格式同样都是osgb&#xff0c;不同软件生产的&#xff0c;建模是参数不一样&#xff0c;还是有很大区别的。尤其在应用阶段。 本文从建模软件、数据组织结构、metadata.xml&#xff08;投影信息&#xff09;、应用几个方面进行了经验性总结。不论您是初步开始建模&#xf…

【QT+QGIS跨平台编译】175:【QGIS_App跨平台编译】—【错误处理:未定义的class APP_EXPORT】

点击查看专栏目录 文章目录 一、未定义的class APP_EXPORT二、错误处理 一、未定义的class APP_EXPORT 报错信息&#xff1a; 二、错误处理 第18行增加&#xff1a; #include "qgis_app.h"

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《新型电力系统多阶段输-储协同分布鲁棒规划》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

Adobe After Effects 2024 v24.3 macOS 视频合成及特效制作软件 兼容 M1/M2/M3

Adobe After Effects 是一款适用于视频合成及特效制作软件,是制作动态影像设计不可或缺的辅助工具,是视频后期合成处理的专业非线性编辑软件。 macOS 12.0及以上版本可用 应用介绍 Adobe After Effects简称 AE 是一款适用于视频合成及特效制作软件,是制作动态影像设计不可或缺…

计算分数和-第12届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第48讲。 计算分数和&#…

STM32 H7系列学习笔记

必备的API知识 第 1 步&#xff1a;系统上电复位&#xff0c;进入启动文件 startup_stm32h743xx.s&#xff0c;在这个文件里面执行复位中断服务程序。 在复位中断服务程序里面执行函数 SystemInit&#xff0c;在system_stm32h7xx.c 里面。*之后是调用编译器封装好的函数&…

Kubesphere 在 devops 部署项目的时候下载 maven 依赖卡住

Kubesphere 在 devops 部署项目的时候下载 maven 依赖卡住 我下载 下面这段 maven 依赖一直卡住&#xff1a; <build><plugins><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>…

LeetCode 80—— 删除有序数组中的重复项 II

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 让 index指向删除重复元素后数组的新长度&#xff1b;让 st_idx 指向重复元素的起始位置&#xff0c;而 i 指向重复元素的结束位置&#xff0c;duplicate_num代表重复元素的个数&#xff1b;一段重复元素结束后&am…

Java Web-分层解耦

三层架构 当我们所有代码都写在一起时&#xff0c;代码的复用性差&#xff0c;并且难以维护。就像我们要修改一下服务端获取数据的方式&#xff0c;从文本文档获取改为到数据库中获取&#xff0c;就难以修改&#xff0c;而使用三层架构能很好的解决这个问题。 controller: 控…

PHP 中的 $2y$10,PHP 字符串加密函数 password_hash

PHP 用户密码加密函数 password_hash 自PHP5.5.0之后&#xff0c;新增加了密码散列算法函数(password_hash)&#xff0c;password_hash() 使用足够强度的单向散列算法创建密码的散列 Hash。 password_hash() 兼容 crypt()。 所以&#xff0c; crypt() 创建的密码散列也可用于 …

flask 访问404

当你的项目有自己的蓝图&#xff0c;有添加自己的前缀&#xff0c;也注册了蓝图。 在访问的路由那里也使用了自己的蓝图&#xff0c;如下图 然后你访问的地址也没问题&#xff0c;但是不管怎么样访问就是返回404&#xff0c;这个时候不要怀疑你上面的哪里配置错误&#xff0c;…

【网站项目】校园二手交易平台小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

外包干了25天,技术倒退明显

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01; 而我已经在一个企业干了四年的功能…

【mT5多语言翻译】之五——训练:中央日志、训练可视化、PEFT微调

请参考本系列目录&#xff1a;【mT5多语言翻译】之一——实战项目总览 [1] 模型训练与验证 在上一篇实战博客中&#xff0c;我们讲解了访问数据集中每个batch数据的方法。接下来我们介绍如何训练mT5模型进行多语言翻译微调。 首先加载模型&#xff0c;并把模型设置为训练状态&a…

网络安全指南:安全访问 Facebook 的技巧

在当今数字化时代&#xff0c;网络安全问题越来越受到人们的关注。尤其是在社交媒体平台上&#xff0c;如 Facebook 这样的巨头&#xff0c;用户的个人信息和隐私更容易受到威胁。为了保护自己的在线安全&#xff0c;我们需要采取一些措施来确保在使用 Facebook 时能够安全可靠…

C语言进阶|顺序表

✈顺序表的概念及结构 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串.. 线性表在逻辑上是线性结构&#xff0c;也就说是连…

大话设计模式——23.备忘录模式(Memento Pattern)

简介 又称快照模式&#xff0c;在不破坏封装性的前提下&#xff0c;捕获一个对象的内部状态&#xff0c;并且该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态 UML图 应用场景 允许用户取消不确定或者错误的操作&#xff0c;能够恢复到原先的状态游戏存档、…

深度学习架构(CNN、RNN、GAN、Transformers、编码器-解码器架构)的友好介绍。

一、说明 本博客旨在对涉及卷积神经网络 &#xff08;CNN&#xff09;、递归神经网络 &#xff08;RNN&#xff09;、生成对抗网络 &#xff08;GAN&#xff09;、转换器和编码器-解码器架构的深度学习架构进行友好介绍。让我们开始吧&#xff01;&#xff01; 二、卷积神经网络…

【动手学深度学习】15_汉诺塔问题

注&#xff1a; 本系列仅为个人学习笔记&#xff0c;学习内容为《算法小讲堂》&#xff08;视频传送门&#xff09;&#xff0c;通俗易懂适合编程入门小白&#xff0c;需要具备python语言基础&#xff0c;本人小白&#xff0c;如内容有误感谢您的批评指正 汉诺塔&#xff08;To…

c/c++ |游戏后端开发之skynet

作者眼中的skynet 有一点要说明的是&#xff0c;云风至始也没有公开说skynet专门为游戏开发&#xff0c;换句话&#xff0c;skynet 引擎也可以用于web 开发 贴贴我的笔记 skynet 核心解决什么问题 愿景&#xff1a;游戏服务器能够充分利用多核优势&#xff0c;将不同的业务放在…