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

概述

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

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

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


问题描述

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

理解这个问题的关键是理解 “快照”。 所谓 “快照” ,指我们为容器创建迭代器时,相当于给容器拍了一张快照(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.mzph.cn/news/805586.shtml

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

相关文章

【IT资质合集】CMMI软件能力成熟度介绍,一定不要错过!

近几年&#xff0c;IT企业在市场上逐渐增多&#xff0c;很多企业为了能在市场上取得一定的发展&#xff0c;那办理几项企业相关资质是不可缺少的&#xff0c;任何一项资质对于企业来说都是具有一定优势的&#xff0c;同时也会更加利于企业的发展。 企业常办理的IT资质类 ✅ C…

C语言每日一题(67)长度最小的子数组

题目链接 209 长度最小的子数组 题目描述 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c…

宠物救助系统|基于Springboot和vue的流浪猫狗救助救援系统设计与实现(源码+数据库+文档)

宠物救助目录 基于Springboot和vue的流浪猫狗救助救援系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、前台&#xff1a; 2、后台 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌…

面试经典算法系列之链表2 -- 环形链表

面试经典算法8-环形链表 LeetCode.141 公众号&#xff1a;阿Q技术站 问题描述 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&am…

flask 后端 + 微信小程序和网页两种前端:调用硬件(相机和录音)和上传至服务器

选择 flask 作为后端&#xff0c;因为后续还需要深度学习模型&#xff0c;python 语言最适配&#xff1b;而 flask 框架轻、学习成本低&#xff0c;所以选 flask 作为后端框架。 微信小程序封装了调用手机硬件的 api&#xff0c;通过它来调用手机的摄像头、录音机&#xff0c;…

【C++成长记】C++入门 |函数重载、引用、内联函数

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;C❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、函数重载 1、函数重载概念 二、引用 1、引用概念 2、引用特性 3、常引用 4、使用场景 5、…

数据库之DCL操作(用户、访问权限。)

DCL英文全称是Data control language(数据控制语言)&#xff0c;用来管理数据库用户、控制数据库的访问权限。 1.管理用户 1.1查询用户 select * from mysql.user; 其中 Host代表当前用户访问的主机&#xff0c;如果为localhost&#xff0c;仅代表只能够在当前本机访问&…

Synergy错误: NOTE: Cursor is locked to screen, check Scroll Lock key

错误&#xff1a; NOTE: Cursor is locked to screen, check Scroll Lock key NOTE: Cursor is locked to screen, check Scroll Lock key NOTE: Cursor is locked to screen, check Scroll Lock key NOTE: Cursor is locked to screen, check Scroll Lock key NOTE: Cursor is…

深入浅出 -- 系统架构之微服务中OpenFeign最佳实践

前面我们讲了一下 Ribbon 和 RestTemplate 实现服务端通信的方法&#xff0c;Ribbon 提供了客户端负载均衡&#xff0c;而 RestTemplate 则对 http 进行封装&#xff0c;简化了发送请求的流程&#xff0c;两者互相配合&#xff0c;构建了服务间的高可用通信。 但在使用后也会发…

谁在投资“元素周期表”? 顶级芯片制造商“军备竞赛”

有色和商品基金的大买家何在 投资A股&#xff0c;有时候投资的也是一种“玄妙”的境界。 你需要复习金融知识、复习经济知识&#xff0c;复习科技知识&#xff0c;学习财政学、学习人口学、学习传染病学。 但这些可能还不够。 你能想象么有朝一日&#xff0c;你会回头复习中…

Flask项目如何在测试环境和生产环境部署上线

前言 最近在使用Flask框架&#xff0c;写一个小项目&#xff0c;在项目部署启动后&#xff0c;出现了以下这段提示&#xff0c;这段提示的意思是&#xff0c;该启动方式适用于开发环境中&#xff0c;生产环境要使用WSGI服务器。 WARNING: This is a development server. Do no…

阿里云大学考试Java中级题目及解析-java中级

阿里云大学考试Java中级题目及解析 1.servlet释放资源的方法是&#xff1f; A.int()方法 B.service()方法 C.close() 方法 D.destroy()方法 D servlet释放资源的方法是destroy() 2.order by与 group by的区别&#xff1f; A.order by用于排序&#xff0c;group by用于排序…

从0到1一步一步玩转openEuler--02 openEuler操作系统的安装

从0到1一步一步玩转openEuler–02 openEuler操作系统的安装 安装地址&#xff1a;https://www.jianshu.com/p/f8b8c7b4cc11

OSCP靶场--Zino

OSCP靶场–Zino 考点(CVE-2019-9581 RCE 定时任务脚本可写提权) 1.nmap扫描 ##┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.173.64 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-10 04:18 EDT Nmap scan report for 192.…

自定义注解进行数据转换

前言&#xff1a; Java注解是一种元数据机制&#xff0c;可用于方法&#xff0c;字段&#xff0c;类等程序上以提供关于这些元素的额外信息。 以下内容是我自己写的一个小测试的demo,参考该文章进行编写&#xff1a;https://blog.csdn.net/m0_71621983/article/details/1318164…

【linux】基础IO(四)

在上一篇基础IO中我们主要讲述了文件再磁盘中的存储&#xff0c;当然我们说的也都只是预备知识&#xff0c;为这一篇的文件系统进行铺垫。 目录 搭文件系统的架子&#xff1a;填补细节&#xff1a;inode&#xff1a;datablock[]: 更上层的理解&#xff1a; 搭文件系统的架子&a…

dynamicreports示例

1. 简单段落文本报表 //标题样式StyleBuilder titleStyle DynamicReports.stl.style().setHorizontalTextAlignment(HorizontalTextAlignment.CENTER)//设置对齐方式.setFontSize(50)//设置字体.setBackgroundColor(Color.CYAN);//设置背景颜色//段落样式StyleBuilder paragra…

uniapp 2.0可视化工具:创建与管理Vue文件的实践之旅

引言 在前端开发领域中&#xff0c;Vue以其简洁、易上手的特点&#xff0c;受到了广大开发者的青睐。随着uniapp的不断发展&#xff0c;越来越多的开发者开始利用uniapp的可视化工具来创建和管理Vue文件&#xff0c;以提高开发效率。本文将详细介绍如何使用uniapp 2.0可视化工…

bytetrack复现

一,环境安装 创建虚拟环境 conda create -n bytetrack python=3.8 安装requirements pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 可能报错,解决办法,安装numpy 安装 pytorch pip install torch==1.12.0+cu113 torchvision==0.13.0+cu1…

redis修改协议改了,有哪些替代品?

Redis 是一款广泛使用的开源内存数据结构存储&#xff0c;它支持多种数据结构&#xff0c;如字符串、哈希表、列表、集合、有序集合等。然而&#xff0c;由于 Redis 最近更改了其开源许可证&#xff0c;一些用户和开发者可能正在寻找替代品。以下是一些 Redis 的替代品&#xf…