设计模式学习笔记 - 设计模式与范式 -行为型:10.迭代器模式(中):遍历集合时,为什么不能增删集合?

概述

上篇文章,我们通过给 ArrayList LinkedList 容器实现迭代器,学习了迭代器模式的原理、实现和设计意图。迭代器模式主要主要是解耦容器代码和遍历代码。

本章,我们来深挖一下,如果在使用迭代器遍历集合的同时增加、删除集合元素,会发生什么情况呢?该如何应对?如何在遍历的同时安全地删除集合元素?


在遍历的同时增删集合元素会发生什么?

通过迭代器遍历集合元素的同时,增加或删除元素,可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有时候也可以正常遍历,所以,这种行为成为结果不可预期行为未决行为。也就是说,运行结果到底是对还是错,要视情况而定。

通过一个例子来解释一下。我们还是延续上篇文章实现的 ArrayList 迭代器的例子。相关代码重新拷贝到了这里。

public interface Iterator<E> {
    boolean hasNext();
    void next();
    E currentItem();
}

public class ArrayIterator<E> implements Iterator<E> {
    private int cursor;
    private ArrayList<E> arrayList;

    public ArrayIterator(ArrayList<E> arrayList) {
        this.cursor = 0;
        this.arrayList = arrayList;
    }

    @Override
    public boolean hasNext() {
        return cursor != arrayList.size(); // 注意这里,cursor在指向最后一个元素的时候,hasNext()仍返回true
    }

    @Override
    public void next() {
        cursor++;
    }

    @Override
    public E currentItem() {
        if (cursor >= arrayList.size()) {
            throw new NoSuchElementException();
        }
        return arrayList.get(cursor);
    }
}

public interface List<E> {
    Iterator iterator();
}

public class ArrayList<E> implements List<E> {
    // ...
    @Override
    public Iterator iterator() {
        return new ArrayIterator(this);
    }
    // ...
}

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        iterator.next();
        names.remove("a");
    }
}

ArrayList 的底层对应的是数组这种数据结构,在执行 Iterator<String> iterator = names.iterator() 时,数组中存储的是 a、b、c、d 四个元素,迭代器的游标 cursor 指向元素 a。当执行完 iterator.next() 时,游标指向 b,到这里没有问题。

为了保持数组存储数据的连续性,数组的删除操作会涉及元素的移动。当执行到 names.remove("a") 时,程序从数据中将元素 a 删除,b、c、d 依次往前移动一位,这就会导致游标本来指向元素 b,现在变成执行元素 c 了。原本在执行完 iterator.next() 后,我们还可以遍历 b、c、d 三个元素,但是在执行 names.remove("a") 之后,就只能遍历到 c、d 两个元素,b 遍历不到了。

上述过程如下图所示。

在这里插入图片描述
不过,如果删除的不是游标前面的元素(元素 a)及游标所在的位置的元素(元素 b),而是游标后面的元素(元素 c、d),这样就不会存在任何问题了,不会存在某个元素遍历不到的情况。

所以,在前面会说,在遍历的过程中删除元素,结果是不可预期的。

在遍历的过程中删除集合元素,有可能会导致某个元素遍历不到,那在遍历的过程中添加集合元素,会发生什么情况呢?

还是结合刚刚的例子,稍微改造下代码,把删除改为添加元素。

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        iterator.next();
        names.add(0, "x");
    }
}

当执行完 iterator.next() 时,数组中包含 a、b、c、d 四个元素,游标指向 b。

在执行 names.add(0, "x") 之后,将 x 插入到下标为 0 的位置, a、b、c、d 四个元素依次向后移动一位。此时,游标又重新指向了 a。元素 a 被游标重复指向了两次,也就是说,元素 a 存在被重复遍历的情况。

跟删除类似,如果我们在游标的后面添加元素,就不会存在任何问题。所以,在遍历的同时添加元素也是一种不可预期行为。

在这里插入图片描述

如何应对遍历时改变集合导致的未决行为?

当通过迭代器遍历集合数据时,增加、删除集合元素会导致不可预期的遍历结果。实际上,“不可预期” 比直接出错更加可怕,有时候运行正确,有时候运行错误,一些隐藏很深、很难 debug 的 bug 就是这么产生的。那如何才能避免
出现这种不可预期的运行结果呢?

有两种解决方案:一种是遍历的时候不允许删除增删元素,另一种是增删元素之后让遍历报错。

第一种解决方式:不允许删除增删元素

实际上,第一种解决方案比较难实现,我们需要确定遍历开始和结束的时间点。遍历开始时间很容易获得。可以把迭代器的创建时间。但是遍历结束的时间点该如何确定呢?

可能你会说,遍历到最后一个元素的时候就结束。但是,在实际的开发中,每次使用迭代器来遍历元素,并不一定非要把所有元素遍历一遍。如下所示,代码找到一个值为 b 的元素就提前结束了遍历。

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        while (iterator.hasNext()) {
            String name = iterator.currentItem();
            if (name.equals("b")) {
                break;
            }
        }
    }
}

你可能还会说,可以在迭代器中定义一个新的接口 finishIteration(),主动告知容器迭代器使用完了,你可以增删元素了,代码如下所示。但是,这就要求程序员在使用完迭代器之后要主动调用这个函数,也增加了开发成本,还很容易漏掉。

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        while (iterator.hasNext()) {
            String name = iterator.currentItem();
            if (name.equals("b")) {
                iterator.finishIteration(); // 主动告知容器这个迭代器用完了
                break;
            }
        }
    }
}

第二种解决方式: 增删元素之后让遍历报错

实际上,第二种解决方式更加合理。在 Java 中就是采用的这种解决方案,增删元素之后,让遍历报错。接下来看下,具体如何实现。

怎么确定在遍历时,集合有没有增删元素呢?

ArrayList 中定义一个变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素,就会给 modCount 加 1.当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()next()currentItem() 函数,都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否被改变过。

如果两个值不同,那就说明集合存储的元素被修改了,之前创建的迭代器就不能正确运行了,再继续使用就会产生不可预期结果,所以我们选择 fail-fast 解决方式,抛出运行时异常。

上面描述的代码实现如下所示。

public class ArrayIterator<E> implements Iterator<E> {
    private int cursor;
    private ArrayList<E> arrayList;
    private int expectedModCount;

    public ArrayIterator(ArrayList<E> arrayList) {
        this.cursor = 0;
        this.arrayList = arrayList;
        this.expectedModCount = arrayList.modCount;
    }

    @Override
    public boolean hasNext() {
        checkForComodification();
        return cursor != arrayList.size(); // 注意这里,cursor在指向最后一个元素的时候,hasNext()仍返回true
    }

    @Override
    public void next() {
        checkForComodification();
        cursor++;
    }

    @Override
    public E currentItem() {
        checkForComodification();
        if (cursor >= arrayList.size()) {
            throw new NoSuchElementException();
        }
        return arrayList.get(cursor);
    }

    private void checkForComodification() {
        if (arrayList.modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        iterator.next();
        names.remove("a");
        iterator.next(); // 抛出ConcurrentModificationException异常
    }
}

如何在遍历的同时安全地删除集合元素?

像 Java 语言,迭代器类中除了前面提到的几个最基本的方法外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。不过,需要说明的是,Java 并没有提供安全地添加元素的方法。毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适。

个人觉得,Java 迭代器中提供的 remove() 方法还是比较鸡肋的。它只能删除游标指向的前一个元素,而且 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");

        Iterator<String> iterator = names.iterator();
        iterator.next();
        iterator.remove();
        iterator.remove(); // 抛出IllegalStateException异常
    }
}

现在,我们来看下,为什么通过迭代器就能安全地删除集合中的元素呢?我们看下 remove() 函数是如何实现的,代码如下所示。

提醒下,Java 的实现中,迭代器类是容器的内部类,并且 next() 函数不仅将游标后移一位,还会返回当前的元素。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // ...
	transient Object[] elementData;
	private int size;
	// ...
	public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
		// ...
    }
    // ...
}

上面的迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素。通过迭代器去删除这个元素的时候,可以通过更新迭代器的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到。如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,那我们就要维护这个容器都创建了哪些迭代器了,每个迭代器是否还在使用信息,代码实现就比较复杂了。

总结

在通过迭代器来遍历集合元素的同时增删元素,可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有的情况下都会遍历出错,有时候也能正常遍历,所以,这种行为成为不可预期行为或未决行为。实际上,“不可预期” 比直接出错更加可怕,一些隐藏很深、很难 debug 的 bug 就是这么产生的。

有两种解决方案,来避免出现这种不可预期的运行结果。

  • 一种是遍历时,不允许增删元素。
  • 另一种是增删元素之后,遍历报错。

第一种实现方式比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理。

Java 语言中的迭代器就是采用的第二种方案。增删元素之后,选择 fail-fast 解决方式,让遍历直接抛出运行时异常。

Java 语言,迭代器还提供了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。

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

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

相关文章

无尘净化棉签:清洁革新的里程碑

随着科技的不断进步&#xff0c;日常生活中的许多小物件也在不断地得到创新和改良。其中&#xff0c;棉签作为一种常见的清洁工具&#xff0c;经历了从传统到现代的革新&#xff0c;引入了无尘棉签的概念&#xff0c;为清洁领域带来了一场革命性的变革。本文优斯特将探讨无尘棉…

运维工具-Backup集合

RepositoryLicenseStarCreatedAtUpdatedAtDescriptionjeessy2/backup-xMIT2842021-11-132023-12-15带Web界面的数据库/文件备份增强工具noovertime7/gin-mysqlbakMIT382022-06-212023-02-06一款分布式高性能的备份系统&#xff0c;支持 MySQL、ElasticSearch 备份&#xff0c;多…

《高通量测序技术》分享,生物信息学生信流程的性能验证,以肿瘤NGS基因检测为例。

这是这本书&#xff0c;第四章第五节的内容&#xff0c;这一部分是以NGS检测肿瘤基因突变为例&#xff0c;描述了其原理和大概流程&#xff0c;这和以前我分享的病原宏基因组高通量测序性能确认方案可以互相补充&#xff0c;大家可以都看一下&#xff0c;但是想要真正的弄懂&am…

【Leetcode】1702. 修改后的最大二进制字符串

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 给你一个二进制字符串 b i n a r y binary binary &#xff0c;它仅有 0 0 0 或者 1 1 1 组成。你可以使用下面的操作任意次对它进行修改&#xff1a; 操作 1 &#xff1a;如果…

背 单 词

单词&#xff1a; 买考研词汇闪过 研究艾宾浩斯遗忘曲线 https://www.bilibili.com/video/BV18Y4y1h7YR/?spm_id_from333.337.search-card.all.click&vd_source5cbefe6dd70d6d84830a5891ceab2bf9 单词方法 闪记背两排&#xff08;5min&#xff09;重复一遍&#xff08;2mi…

解决Can‘t connect to HTTPS URL because the SSL module is not available

把C:\develop\An3\Library\bin的这些文件&#xff0c;复制到C:\develop\An3\DLLs中

2006年重邮801信号与系统考研真题与详解

本系列文章为重庆邮电大学801信号与系统考研真题与详解&#xff0c;前面文章链接如下&#xff1a; 2003年重邮801信号与系统考研真题与详解 2004年重邮801信号与系统考研真题与详解 2005年重邮801信号与系统考研真题与详解 文章目录 前言一对一极速提分辅导2006年重邮801信号与…

基于GAN的图像补全实战

数据与代码地址见文末 论文地址:http://iizuka.cs.tsukuba.ac.jp/projects/completion/data/completion_sig2017.pdf 1.概述 图像补全,即补全图像中的覆盖和缺失部分, 网络整体结构如下图所示,整体网络结构还是采取GAN,对于生成器,网络结构采取Unet的形式,首先使用卷积…

字节发布AnimateDiff-Lightning文生视频模型——可在线免费试玩

Sora文生视频大模型 随着Sora文生视频大模型的爆火&#xff0c;文生视频大模型必定是各大人工智能公司竞争的主要领域。虽然 Sora模型的视频效果绝对是领先地位&#xff0c;但是Sora模型目前还没有开放使用&#xff0c;我们并无法直接使用。前期我们也介绍过字节发布的MagicVi…

【优选算法专栏】专题十三:队列+宽搜(一)

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

C++ 线程库(thread)与锁(mutex)

一.线程库(thread) 1.1 线程类的简单介绍 thread类文档介绍 在C11之前&#xff0c;涉及到多线程问题&#xff0c;都是和平台相关的&#xff0c;比如windows和linux下各有自己的接口&#xff0c;这使得代码的可移植性比较差。C11中最重要的特性就是对线程进行支持了&#xff…

群联AI云防护中的防盗链技术原理及其作用探析---

一、引言 随着云计算和AI技术的快速发展&#xff0c;云防护方案已经成为现代企业防范网络攻击和保护数字资产的重要手段之一。群联科技作为存储解决方案和技术服务的领导者&#xff0c;已将其AI技术应用于云端防护系统中&#xff0c;并特别强化了防盗链功能&#xff0c;以帮助…

element-plus blur和select冲突失效导致移除焦点清空文本框内容失效解决方案

问题 需求是做一个查询企业的功能&#xff0c;期望必须进行选择后进行查询&#xff0c;如果用户自主输入并没有进行选择&#xff0c;失去焦点的时候清空文本框里面的内容&#xff0c;给el-autocomplete添加blur事件后发现&#xff0c;在下拉没有出现之前快速失去焦点才能触发b…

SuperGluePretrainedNetwork调用接口版本(两个版本!)

本脚本是一个基于Python的应用&#xff0c;旨在演示如何使用SuperGlue算法进行图像之间的特征匹配。SuperGlue是一个强大的特征匹配工具&#xff0c;能够在不同的图像之间找到对应的关键点。这个工具尤其适用于计算机视觉任务&#xff0c;如立体视觉、图像拼接、对象识别和追踪…

express操作mysql数据库的方法总结

作为前端&#xff0c;我们无需去考虑数据库的问题&#xff0c;业务场景需要的话&#xff0c;我们可以mock数据&#xff0c;满足暂时的联调场景。但是对于数据库&#xff0c;我们前端可以不用&#xff0c;却不能不了解不懂。所以这篇文章整理下&#xff0c;nodejs框架express中怎…

【通信原理笔记】【三】模拟信号调制——3.5 角度调制(FM、PM)与其频谱特性

文章目录 前言一、相位与频率二、PM和FM的数学表示三、FM的频谱四、FM信号的带宽——卡松公式总结 前言 在之前介绍的几种调制方式中&#xff0c;我提到信噪比时计算的是用户解调后的信噪比&#xff0c;然而在北邮通信原理课中考虑的是解调器输入的信噪比&#xff0c;即考虑的…

H-GAP: Humanoid Control with a Generalist Planner

ICLR 2024 paper Intro 本文方法研究利用大量人类动捕数据以及衍生的类人轨迹数据&#xff0c;基于MPC实现下游任务中机器人运动控制。 method H-GAP 的算法框架分为三个部分&#xff1a;基于VQ-VAE对状态动作序列的离散化&#xff0c;基于Transformer对latent code的先验…

爬虫 新闻网站 以湖南法治报为例(含详细注释,控制台版) V2.0 升级自定义查询关键词、时间段

目标网站&#xff1a;湖南法治报 爬取目的&#xff1a;为了获取某一地区更全面的在湖南法治报已发布的宣传新闻稿&#xff0c;同时也让自己的工作更便捷 环境&#xff1a;Pycharm2021&#xff0c;Python3.10&#xff0c; 安装的包&#xff1a;requests&#xff0c;csv&#xff…

uniapp 2.0可视化开发工具高级事件使用技巧探索

摘要 随着移动应用市场的不断扩大和前端技术的飞速发展&#xff0c;开发者们对于快速、高效构建跨平台应用的需求日益增强。uniapp作为一款优秀的跨平台应用开发框架&#xff0c;凭借其强大的功能和易用的特性&#xff0c;赢得了广大开发者的青睐。在uniapp 2.0版本中&#xf…

基于SpringBoot + Vue实现的在线答疑系统设计与实现+毕业论文+答辩PPT

介绍 学生角色&#xff1a; 1.注册、登录功能&#xff1a;学生可以通过系统完成注册和登录操作&#xff0c;进入学生专属界面。 2.个人信息修改功能&#xff1a;学生可以查看和修改自己的个人信息&#xff0c;包括姓名、联系方式等。 3.问题发布功能&#xff1a;学生可以在线发…