把书读薄 | 《设计模式之美》设计模式与范式(行为型-迭代器模式)(下)

简介: 本文对应设计模式与范式:行为型(65-67),迭代器模式 (Iterator Pattern),又称 游标模式,用于 解耦容器代码和遍历代码。 不过,很多编程语言都将迭代器作为一个基础类库,直接提供出来了。日常业务开发,很少自己实现一个迭代器,当然,弄懂原理能帮助我们更好地使用这些工具类~

具体容器类返回迭代器createIterator()方法,改成new OrderTimeIterator()即可,输出结果如下:


网络异常,图片无法展示
|


不懂快排的童鞋不需要了解具体细节,直接换迭代器即可,还可以按照自己的需求自定义迭代器,妙啊。


对了foreach循环语法糖,其实也是基于迭代器实现的,接着带出UML类图、使用场景和优缺点:


网络异常,图片无法展示
|


使用场景


  • 希望对客户端隐藏遍历算法复杂性时;
  • 需为容器(聚合)对象提供多种遍历方式时;


优点


  • 满足单一职责原则和开闭原则;
  • 更好的封装性,简化客户端调用,可以用不同的变量方式来遍历一个集合;


缺点


  • 子类增加;
  • 对于简单遍历,略显繁琐,如ArrayList直接用for循环+get()遍历即可;
  • 抽象迭代器的设计难度大,需要充分考虑到系统将来的扩展,如JDK内置迭代器Iterator就无法实现逆向遍历。如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情


0x3、加餐1:fail-first 快速机制


问题来了 → 当遍历的同时增删集合元素会怎么样


答:可能会导致重复遍历或遍历不到某个元素。


是可能,并不会所有情况下都遍历出错,有时还可以正常遍历,这种行为称为 结果不可预期行为未决行为,即运行的结果是对是错,得是情况而定。


比如原列表长度为5,迭代的时候插入了一个元素,但迭代器length还是之前的5,会漏掉新插入的元素; 又比如迭代时删掉了最后一个元素,但迭代器length还是之前的5,会引发数组越界;


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


  • 遍历时不允许增删元素
  • 遍历时增删元素直接报错


方法一比较难实现,得确定遍历开始与结束的时间点,开始好拿(如创建迭代器时),结束不好拿,因为遍历不一定把所有元素都走一遍,比如找到满足条件的元素,提前结束遍历。


在迭代器内定义一个接口finishIteration(),主动告知容器迭代器使用完毕,但这就要求调用者在使用完迭代器后要主动调用此函数,增加了开发成本之余还容易漏掉。


Java语言中采用的方法二,如ArrayList中定义了一个成员变量modCount,记录集合被修改的次数,调用增删函数都会加1。


创建迭代器的时候传入,然后每次调用迭代器的next()、hasNext()函数时都检查集合中的modCount是否等于一开始传入的modCount,不等说明集合存储的元素已经发生改变,之前创建的迭代器已不能正确运行,直接抛出运行时异常,结束程序。


另外,在单线程情况下,ArrayList使用迭代器进行迭代,通过迭代器增删元素,不会引发异常,原理是:


内部类Itr 实现Iterator接口,定义了两个变量cursor (下一个元素下标) 和 lastRet (上一个元素下标) 当发生元素增删时,更新迭代器中的游标及这两个值,保证遍历不出错。


而对于多线程的情况,除了在iterator使用处加锁外,还可以用 并发容器


原理是:采用的是 fail-safe(安全失败) 机制:迭代时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以遍历期间原集合发生的修改迭代器是不知道的,原迭代器也不能访问修改后的内容。Java的并发容器放在java.util.concurrent包中,如使用 CopyOnWriteArrayList 来代替ArrayList。


0x4、加餐2:实现一个支持快照功能的迭代器


所谓的 "快照" 就是创建迭代器时相当于给容器拍了张快照(Snapshot),之后增删容器元素,快照中的元素都不会发生改变,即迭代器遍历的对象是快照而非容器。通过一个例子来解释这段话:


List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
ListIterator<String> it1 = list.listIterator();
while (it1.hasNext()) System.out.print(it1.next()); // 输出: abcd
System.out.println();
list.remove("a");
ListIterator<String> it2 = list.listIterator();
while (it2.hasNext()) System.out.print(it2.next()); // 输出:bcd
System.out.println();
list.remove("c");
ListIterator<String> it3 = list.listIterator();
while (it3.hasNext()) System.out.print(it3.next()); // 输出:bd


第一种解法:迭代器类中定义一个存储快照的成员变量,构造迭代器时复制原集合引用进行初始化,后续遍历都基于持有的快照进行。(我上面定义的OrderTimeIterator就是这种)。


当然,缺点明显,每次创建迭代器,都要拷贝一份数据到快照中,增加内存消耗,当有多个迭代器在遍历元素,还会导致重复存储多份。不过,好在Java中的拷贝属于浅拷贝,所以只是拷贝了对象的引用而已。


第二种解法:容器中为每个元素保存两个时间戳,添加时间删除时间 (初始化为最大长整型值),添加时将添加时间设置为当前时间,删除时将时间设置为当前时间,记住只是 标记删除,并非真的从容器中将其删除


然后每个迭代器保存一个 创建时间,即快照创建时间戳,当使用迭代器遍历容器时,只有满足:


添加时间 < 创建时间 < 删除时间


的元素才属于这个迭代器的快照:


  • 添加时间 > 创建时间 → 说明元素在创建迭代器后才加入,不属于这个迭代器的快照;
  • 删除时间 < 创建事件 → 说明元素在创建迭代器前就被删除了,同样不属于这个迭代器的快照;


在不拷贝容器的情况下,在容器本身借助时间戳实现快照功能,妙啊!


这种方式解决了一个问题,又引入了一个问题:


ArrayList底层依赖数组这种存储结构,原本支持快速随机访问,在O(1)时间复杂度内获取下标为i的元素。但现在删除元素并没有真正删除,这就导致无法支持按照下标快速随机访问了。


解法:


在ArrayList中存储两个数组,一个支持标记删除,用来实现快照遍历,一个不支持标记删除(删除数据直接从数组中删除),用来支持随机访问。


以上内容就是本节的全部内容,谢谢~


相关文章
|
2月前
|
设计模式 Java Kotlin
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
55 2
|
2月前
|
设计模式 Java Kotlin
Kotlin - 改良设计模式 - 迭代器模式
Kotlin - 改良设计模式 - 迭代器模式
37 0
|
3月前
|
设计模式 Java 开发者
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。对于快速学习Kotlin的用户,推荐查看“简洁”系列教程。本文重点介绍迭代器模式,通过具体示例展示了如何在Kotlin中实现迭代器模式,包括使用Iterator、Iterable接口及重载iterator运算符的方法。
42 4
|
3月前
|
设计模式 Java Kotlin
Kotlin学习笔记 - 改良设计模式 - 迭代器模式
Kotlin学习笔记 - 改良设计模式 - 迭代器模式
45 2
|
3月前
|
设计模式 Java 开发者
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
本教程详细讲解了Kotlin中的迭代器模式,包括如何通过实现Iterator和Iterable接口以及重载iterator运算符来实现可遍历的自定义集合。示例展示了如何创建一个图书集类,并通过不同方式使其支持遍历操作,适合希望深入了解Kotlin迭代器模式的开发者。
42 3
|
3月前
|
设计模式 Java Kotlin
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
42 1
|
2月前
|
设计模式 Java Kotlin
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
Kotlin教程笔记(54) - 改良设计模式 - 迭代器模式
41 0
|
1月前
|
设计模式 前端开发 搜索推荐
前端必须掌握的设计模式——模板模式
模板模式(Template Pattern)是一种行为型设计模式,父类定义固定流程和步骤顺序,子类通过继承并重写特定方法实现具体步骤。适用于具有固定结构或流程的场景,如组装汽车、包装礼物等。举例来说,公司年会节目征集时,蜘蛛侠定义了歌曲的四个步骤:前奏、主歌、副歌、结尾。金刚狼和绿巨人根据此模板设计各自的表演内容。通过抽象类定义通用逻辑,子类实现个性化行为,从而减少重复代码。模板模式还支持钩子方法,允许跳过某些步骤,增加灵活性。
102 11
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
6天前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
65 40