设计模式之美
王争
前Google工程师,《数据结构与算法之美》专栏作者
立即订阅
21545 人已学习
课程目录
已更新 70 讲 / 共 102 讲
0/6登录后,你可以任选6讲全文学习。
开篇词 (1讲)
开篇词 | 一对一的设计与编码集训,让你告别没有成长的烂代码!
免费
设计模式学习导读 (3讲)
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
设计原则与思想:面向对象 (11讲)
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
13 | 实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
设计原则与思想:设计原则 (12讲)
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?
24 | 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
25 | 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
26 | 实战二(下):如何实现一个支持各种统计规则的性能计数器?
设计原则与思想:规范与重构 (11讲)
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
30 | 理论四:如何通过封装、抽象、模块化、中间层等解耦代码?
31 | 理论五:让你最快速地改善代码质量的20条编程规范(上)
32 | 理论五:让你最快速地改善代码质量的20条编程规范(中)
33 | 理论五:让你最快速地改善代码质量的20条编程规范(下)
34 | 实战一(上):通过一段ID生成器代码,学习如何发现代码质量问题
35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”
36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
设计原则与思想:总结课 (3讲)
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
39 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(上)
40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)
设计模式与范式:创建型 (7讲)
41 | 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?
42 | 单例模式(中):我为什么不推荐使用单例模式?又有何替代方案?
43 | 单例模式(下):如何设计实现一个集群环境下的分布式单例模式?
44 | 工厂模式(上):我为什么说没事不要随便用工厂模式创建对象?
45 | 工厂模式(下):如何设计实现一个Dependency Injection框架?
46 | 建造者模式:详解构造函数、set方法、建造者模式三种对象创建方式
47 | 原型模式:如何最快速地clone一个HashMap散列表?
设计模式与范式:结构型 (8讲)
48 | 代理模式:代理在RPC、缓存、监控等场景中的应用
49 | 桥接模式:如何实现支持不同类型和渠道的消息推送系统?
50 | 装饰器模式:通过剖析Java IO类库源码学习装饰器模式
51 | 适配器模式:代理、适配器、桥接、装饰,这四个模式有何区别?
52 | 门面模式:如何设计合理的接口粒度以兼顾接口的易用性和通用性?
53 | 组合模式:如何设计实现支持递归遍历的文件系统目录树结构?
54 | 享元模式(上):如何利用享元模式优化文本编辑器的内存占用?
55 | 享元模式(下):剖析享元模式在Java Integer、String中的应用
设计模式与范式:行为型 (11讲)
56 | 观察者模式(上):详解各种应用场景下观察者模式的不同实现方式
57 | 观察者模式(下):如何实现一个异步非阻塞的EventBus框架?
58 | 模板模式(上):剖析模板模式在JDK、Servlet、JUnit等中的应用
59 | 模板模式(下):模板模式与Callback回调函数有何区别和联系?
60 | 策略模式(上):如何避免冗长的if-else/switch分支判断代码?
61 | 策略模式(下):如何实现一个支持给不同大小文件排序的小程序?
62 | 职责链模式(上):如何实现可灵活扩展算法的敏感信息过滤框架?
63 | 职责链模式(下):框架中常用的过滤器、拦截器是如何实现的?
64 | 状态模式:游戏、工作流引擎中常用的状态机是如何实现的?
65 | 迭代器模式(上):相比直接遍历集合数据,使用迭代器有哪些优势?
66 | 迭代器模式(中):遍历集合的同时,为什么不能增删集合元素?
不定期加餐 (3讲)
加餐一 | 用一篇文章带你了解专栏中用到的所有Java语法
加餐二 | 设计模式、重构、编程规范等相关书籍推荐
春节特别加餐 | 王争:如何学习《设计模式之美》专栏?
免费
设计模式之美
登录|注册

66 | 迭代器模式(中):遍历集合的同时,为什么不能增删集合元素?

王争 2020-04-03
上一节课中,我们通过给 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();
}
@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> {
//...
public Iterator iterator() {
return new ArrayIterator(this);
}
//...
}
public class Demo {
public static void main(String[] args) {
List<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 底层对应的是数组这种数据结构,在执行完第 55 行代码的时候,数组中存储的是 a、b、c、d 四个元素,迭代器的游标 cursor 指向元素 a。当执行完第 56 行代码的时候,游标指向元素 b,到这里都没有问题。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《设计模式之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(20)

  • 小晏子
    思考题:
    1. iterator1 和 iterator2是两个不同的迭代器对象,修改一个不会影响另外一个,所以执行iterator1.remove()后,再执行iterator2.next时,会执行checkForComodification();检查,可是检查条件“arrayList.modCount != expectedModCount”中arrayList的modCount已经变成了5,而此时iterator2的expectedModCount还是4,所以触发ConcurrentModificationException异常。
    2. LinkedList和ArrayList不同是LinkedList底层基于链表实现,增加删除元素不需要移动元素的位置,所以不会出现跟ArrayList不同的情况,比如增加元素时,不论增加的元素时在迭代器前还是后,都能通过指针寻址到下一个元素。
    2020-04-03
    7
  • kyle
    迭代器中删除元素那一段,执行完第57行(删除a以后),游标应该指向c,图中指向d了
    2020-04-03
    2
    4
  • Monday
    hpublic class Demo {
      public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("a");
        names.add("b");
        names.add("c");
        names.add("d");
        Iterator<String> iterator1 = names.iterator();
        Iterator<String> iterator2 = names.iterator();
        iterator1.next();
        iterator1.remove();
        iterator1.next(); // 运行结果?
      }
    }

    哈哈老师的题目笔误了吧。
    运行结果那行应该是iterator2.next()。

    然后结果应该是会抛异常,因为modifyCount不一致了。
    2020-04-03
    2
  • 1.ConcurrentModificationException是在调用迭代器的next方法时产生,因为迭代器2并没有使用,所以不会报错,如果在第13行调用的是iterator2.next()则会报错(原因:expectedModCount在新建迭代器的时候初始化,调用iterator1.remove()只修改iterator1的expectedModCount,不会修改iterator2的,所以在调用iterator2.next()时会报错)
    2.使用迭代器遍历的同时,使用容器的方法进行增删操作也会触发ConcurrentModificationException,行为和ArrayList是一样的

           我有一个问题想问老师,我是培训班出身,而且学历不好,自觉基础不行,所以从工作以来,基本每天都坚持学习,如今已经工作一年多了.可是我每天学习两三个钟头就觉得很累了,脑子像浆糊一样,没办法继续学新东西了,有时学习一整天,从上班开始学,一直学到下班,下班的时候感觉脑子都要扭曲了,好长时间缓解不过来,前几天听说去哪网的前端架构师去世了,年龄才30岁出头,我感觉我保持当下这个状态的话,到不了他的水平就得猝死,我想知道老师是怎么平衡日常生活的?真的有人能坚持每天学习十几个小时吗?这让我觉得特别累,喘不过气来
    2020-04-03
    1
    2
  • 马以
    不会报错
    2020-04-03
    1
    2
  • Ken张云忠
    1.基于文章中给出的 Java 迭代器的实现代码,如果一个容器对象同时创建了两个迭代器,一个迭代器调用了 remove() 方法删除了集合中的一个元素,那另一个迭代器是否还可用?或者,我换个问法,下面代码中的第 13 行的运行结果是什么?
    Exception in thread "main" java.util.ConcurrentModificationException
    因为iterator2.expectedModCount的值与names.modCount的值不相等,expectedModCount比modCount小1.

    2.LinkedList 底层基于链表,如果在遍历的同时,增加删除元素,会出现哪些不可预期的行为呢?
    当在游标及游标之前增删元素时会使有的元素遍历不到;当在游标之后增删元素时无问题.
    LinkedList与ArrayList一样,因为都是集成抽象类java.util.AbstractList,
    在遍历的同时调用两次remove()都会抛出异常,都会抛出的是java.lang.IllegalStateException异常.
    两个迭代器遍历的同时,其中一个迭代器删除元素都会使另一个迭代器抛出java.util.ConcurrentModificationException异常.
    都不支持迭代器里添加元素.
    2020-04-03
    2
    1
  • DexterPoker
    老师的题目是不是
    iterator1.next();
    iterator1.remove();
    iterator2.next(); // 运行结果?
    如果是iterator1,能正常运行;
    如果是iterator2.next();就报错了
    2020-04-03
    1
  • Jackie
    终于明白报ConcurrentModificationException的真正原因了
    2020-04-03
    1
  • Liam
    1 第二个迭代器会报错,modCount发生变化
    2 链表增删不影响游标,不会出现意外
    2020-04-03
    1
  • 柏油
    关于问题2 LinkedList的思考,既然LinkedList是基于链表实现,那在前or后新增删除元素都不会涉及到数据整体的搬移,也就不会出现数据遗漏或者重复处理的情况,咋一看在原集合进行增删操作不会对迭代器的遍历产生影响,那为何LinkedList在有迭代器实例的情况下不允许在原集合进行增删操作呢?源码中hasNext是通过nextIndex < size来判断是否还有元素,在新增删除的情况下对size都有改变;从集合前面删除元素,size减小,迭代器中尾部部分元素无法遍历到;从集合前面新增元素,size增大,迭代器尾部元素hasNext判断中,返回true 但实际已没有可遍历的元素
    2020-04-05
  • 忆水寒
    第一个问题,由于modcount不一样了,所以会出现异常。
    第二个问题,LinkedList和ArrayList行为一致。
    2020-04-05
  • 守拙
    /**课堂讨论
         * 1:
         * 当iterator1调用过remove, iterator2#next()时,
         * Iterator#checkForComodification()会检查 Iterator#expectedModCount
         * 与ArrayList#modCount是否一致.
         * 由于Iterator1#remove()时调用了ArrayList#remove(),
         * 而ArrayList#remove()调用了updateSizeAndModCount()导致ArrayList#modCount发生改变,
         * 所以iterator2#next()会fail-fast.
         *
         * 2:
         * 首先, LinkedList#iterator()返回的iterator实例与ArrayList#iterator()
         * 返回的实例出自于同一个类: AbstractList#Iter.
         * 换句话说, LinkedList#iterator()与ArrayList#iterator()的行为是完全一致的,
         * 会造成同样的不可预期结果.
         *
         * */
    2020-04-04
  • Geek_54edc1
    1、因为modCount和expectModCount不一致,iterator2在遍历时会抛出异常; 2、如果是单链表,如果在游标对应的元素之前增加元素,可能会导致新增加的元素遍历不到;如果删除的恰好是游标对应的元素,可能会导致无效指针错误。
    2020-04-04
  • 每天晒白牙
    思考题1,会报错,iterator2中的 expectedModCount 是最开始的 4,而 names 中的 modCount 是 5,所以报错
    2020-04-03
  • Ken张云忠
    1.基于文章中给出的 Java 迭代器的实现代码,如果一个容器对象同时创建了两个迭代器,一个迭代器调用了 remove() 方法删除了集合中的一个元素,那另一个迭代器是否还可用?或者,我换个问法,下面代码中的第 13 行的运行结果是什么?
    Exception in thread "main" java.util.ConcurrentModificationException
    因为iterator2.expectedModCount的值与names.modCount的值不相等,expectedModCount比modCount小1.

    2.LinkedList 底层基于链表,如果在遍历的同时,增加删除元素,会出现哪些不可预期的行为呢?
    LinkedList与ArrayList一样,因为都是集成抽象类java.util.AbstractList,
    在遍历的同时调用两次remove()都会抛出异常,都会抛出的是java.lang.IllegalStateException异常.
    两个迭代器遍历的同时,其中一个迭代器删除元素都会使另一个迭代器抛出java.util.ConcurrentModificationException异常.
    都不支持迭代器里添加元素.
    2020-04-03
  • 李小四
    设计模式_66:
    # 作业
    1. (代码有错误: 13行应该是`iterator2.next()`), 在`checkForComodification`方法抛出异常。因为`iterator1`remove会导致`iterator2`的`expectedModCount`与集合的`modCount`就不一致。
    2.
    - 删除游标之前元素,会导致遍历了已删除的元素。
    - 增加游标之前的元素,会导致新增元素不被遍历。

    # 感想
    对于“不可预期直接出错更加可怕”感触比较深,因为直接出错的问题一般会在自测(或单元测试)或提测后暴露出来,线上产品不会有问题。于是,“不可预期”的问题更多地会暴露在线上,最终牺牲了用户体验。
    2020-04-03
  • 朱晋君
    无论是ArrayList还是LinkedList,使用iterator的remove方法来remove元素后再遍历,都是不会报错的,使用list中的remove都会报错。因为expectedModCount != modCount
    但是LinkedList删除元素,并不会移动后面的元素,所以不存在文中说的遍历不到的问题
    2020-04-03
  • Heaven
    1.实现中,remove方法中,会在调用集合的remove方法后,将当前的修改量赋值到这个迭代器的内部的修改量属性上,但是对于其他迭代器调用的remove无法感知,自然无法修改本迭代器内部的修改量属性,导致next()会在调用checkForComodification()函数的时候发生报错
    2.LinkedList,在增删的时候,由于双向链表的特性,只能感知到上一位和下一位,所以并不会导致异常情况的发生
    2020-04-03
  • halweg
    小争哥后面是不是还有门系统设计的课,要是再有这门课,我觉得就此生无憾了。答应我,一定要出,好不好?
    2020-04-03
  • test
    1.会报错,modCount变了;
    2.LinkedList也是需要使用迭代器的remove方法,不然会有不可预期行为。
    2020-04-03
收起评论
20
返回
顶部