写点什么

16. 迭代器模式设计思想

作者:杨充
  • 2024-11-25
    湖北
  • 本文字数:8098 字

    阅读完需:约 27 分钟

16.迭代器模式设计思想

目录介绍

  • 01.迭代器模式基础

  • 1.1 迭代器模式由来

  • 1.2 迭代器模式定义

  • 1.3 迭代器模式场景

  • 1.4 迭代器模式思考

  • 1.5 主要解决的问题

  • 02.迭代器模式原理

  • 2.1 罗列一个场景

  • 2.2 用例子理解迭代器

  • 2.3 案例实现过程

  • 2.4 迭代器实现步骤

  • 03.迭代器模式结构

  • 3.1 迭代器标准案例

  • 3.2 迭代器模式结构图

  • 3.3 迭代器模式时序图

  • 04.迭代器模式案例

  • 4.1 Java 中 Iterator 接口

  • 4.2 迭代器遍历案例

  • 4.3 遍历时不建议删除元素

  • 4.4 for 循环 VS 迭代器

  • 4.5 迭代器原理介绍

  • 4.6 迭代器模式优势

  • 05.迭代器者模式分析

  • 5.1 迭代器模式优点

  • 5.2 迭代器模式缺点

  • 5.3 适用环境

  • 5.4 思考题考察

  • 06.观察者模式总结

  • 6.1 总结一下学习

  • 6.2 更多内容推荐

推荐一个好玩网站

一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网


https://yccoding.com/


关于设计模式,所有的代码都放到了该项目。设计模式大全

01.迭代器模式基础

1.1 迭代器模式由来

容器的种类有很多种,比如 ArrayList、LinkedList、HashSet...


  1. 每种容器都有自己的特点,ArrayList 底层维护的是一个数组;LinkedList 是链表结构的;HashSet 依赖的是哈希表,每种容器都有自己特有的数据结构。

  2. 因为容器的内部结构不同,很多时候可能不知道该怎样去遍历一个容器中的元素。所以为了使对容器内元素的操作更为简单,Java 引入了迭代器模式!


不同的集合会对应不同的遍历方法,客户端代码无法复用。在实际应用中如何将两个不同集合整合是相当麻烦的。


所以才有 Iterator,它总是用同一种逻辑来遍历集合。使得客户端自身不需要来维护集合的内部结构,所有的内部状态都由 Iterator 来维护。

1.2 迭代器模式定义

迭代器模式为遍历集合中的元素提供了一种通用接口。


其核心思想是,将集合的遍历逻辑与集合对象的实现分离。通过迭代器,客户端代码无需关心集合的底层实现方式(如数组、链表等),就能顺序地访问集合的每一个元素。

1.3 迭代器模式场景

典型场景如下所示:


  1. Java 集合框架:Java 的 Iterator 接口是迭代器模式的一个经典实现。诸如 List、Set 等集合类都支持使用迭代器来遍历元素。

  2. 文件处理:处理文件中的多行内容时,可以使用迭代器模式来顺序读取每一行,而不需要在意底层文件如何存储。

  3. 数据库结果集遍历:数据库查询返回的结果集可以通过迭代器模式逐条访问,不必关心结果集的存储细节。

1.4 迭代器模式思考

以下是一些关于迭代器模式的思考:


  1. 遍历过程中的修改:在使用迭代器遍历集合时,如果需要对集合进行修改,可能会引发一些问题。

  2. 封装集合:迭代器模式允许将集合的内部实现细节隐藏起来,只暴露一个统一的迭代器接口。

1.5 主要解决的问题

迭代器模式主要解决以下问题:


  1. 隐藏集合的内部结构:迭代器模式允许客户端代码遍历集合中的元素,而无需了解集合的内部结构。

  2. 统一遍历接口:迭代器模式定义了一个统一的迭代器接口,提高代码复用性和可读性。

  3. 安全地遍历集合:迭代器模式可以确保在遍历集合时不会对集合的结构造成破坏。

  4. 支持并发遍历:在多线程环境下,使用迭代器模式可以支持并发遍历集合。

02.迭代器模式原理

2.1 罗列一个场景

用 Java 代码实现一个简单的迭代器模式示例,场景为遍历一个字符串集合。

2.2 用例子理解迭代器

2.3 案例实现过程

1 迭代器接口,定义了遍历集合的方法


interface Iterator<T> {    boolean hasNext();    T next();}
复制代码


2 集合接口,定义了创建迭代器的方法


interface Aggregate<T> {    Iterator<T> createIterator();}
复制代码


3 具体的集合类,包含一个字符串数组


class ConcreteAggregate implements Aggregate<String> {    private String[] items;    private int size;     public ConcreteAggregate(int size) {        this.size = size;        items = new String[size];        for (int i = 0; i < size; i++) {            items[i] = "Item " + (i + 1);        }    }     // 创建具体的迭代器    @Override    public Iterator<String> createIterator() {        return new ConcreteIterator(this);    }     // 获取集合的大小    public int getSize() {        return size;    }     // 通过索引获取集合中的元素    public String getItem(int index) {        if (index < size) {            return items[index];        }        return null;    }}
复制代码


4 具体的迭代器类,负责遍历字符串集合


class ConcreteIterator implements Iterator<String> {    private ConcreteAggregate aggregate;    private int currentIndex = 0;     public ConcreteIterator(ConcreteAggregate aggregate) {        this.aggregate = aggregate;    }     // 判断是否还有下一个元素    @Override    public boolean hasNext() {        return currentIndex < aggregate.getSize();    }     // 获取下一个元素    @Override    public String next() {        return aggregate.getItem(currentIndex++);    }}
复制代码


5 测试迭代器模式


public class IteratorPatternDemo {    public static void main(String[] args) {        // 创建具体集合        ConcreteAggregate aggregate = new ConcreteAggregate(5);         // 创建迭代器        Iterator<String> iterator = aggregate.createIterator();         // 使用迭代器遍历集合        while (iterator.hasNext()) {            System.out.println(iterator.next());        }    }}
复制代码


ConcreteAggregate:表示一个具体的集合,内部存储若干个字符串,并且实现了 Aggregate 接口,提供创建迭代器的功能。


ConcreteIterator:实现 Iterator 接口,内部持有对集合的引用,负责遍历集合中的元素。


IteratorPatternDemo:测试类,创建集合对象,并使用迭代器逐一访问集合中的元素。

2.4 迭代器实现步骤

  1. Iterator(迭代器接口):定义了访问集合元素的方法。通常包括以下常见操作:

  2. hasNext():检查是否还有下一个元素。

  3. next():返回集合中的下一个元素。

  4. remove():删除当前元素(可选)。

  5. ConcreteIterator(具体迭代器类):实现 Iterator 接口,负责遍历具体集合中的元素。

  6. Aggregate(集合接口):定义创建迭代器的接口。集合类通常提供一个 createIterator() 方法,用于返回一个 Iterator 对象。

  7. ConcreteAggregate(具体集合类):实现集合接口,包含若干元素,并能够生成相应的迭代器对象。

  8. Client(客户端):使用迭代器的地方。

03.迭代器模式结构

3.1 迭代器标准案例

3.2 迭代器模式结构图

角色职责说明


  1. Aggregate:定义了创建迭代器的接口,负责管理集合中的元素。

  2. ConcreteAggregate:具体的集合类,实现了创建迭代器的接口,并持有具体的数据。

  3. Iterator:定义了用于遍历集合的方法。

  4. ConcreteIterator:负责具体的迭代操作,内部维护一个指针来跟踪当前访问的元素。

3.3 迭代器模式时序图

04.迭代器模式案例

4.1 Java 中 Iterator 接口

看看 Java 中的 Iterator 接口是如何实现的。 在 Java 中 Iterator 为一个接口,它只提供了迭代的基本规则。


package java.util;public interface Iterator<E> {    boolean hasNext();//判断是否存在下一个对象元素    E next();//获取下一个元素    void remove();//移除元素}
复制代码


Iterable,Java 中还提供了一个 Iterable 接口,Iterable 接口实现后的功能是‘返回’一个迭代器


我们常用的实现了该接口的子接口有:Collection、List、Set 等。该接口的 iterator()方法返回一个标准的 Iterator 实现。实现 Iterable 接口允许对象成为 Foreach 语句的目标。就可以通过 foreach 语句来遍历你的底层序列。


Iterable 接口包含一个能产生 Iterator 对象的方法,并且 Iterable 被 foreach 用来在序列中移动。因此如果创建了实现 Iterable 接口的类,都可以将它用于 foreach 中。


Iterable 接口的具体实现:


import java.util.Iterator;public interface Iterable<T> {    Iterator<T> iterator();}
复制代码

4.2 迭代器遍历案例

如果要问 Java 中使用最多的一种模式,答案不是单例模式,也不是工厂模式,更不是策略模式,而是迭代器模式,先来看一段代码吧:


public static void print(Collection coll){      Iterator it = coll.iterator();      while(it.hasNext()){          String str = (String)it.next();          System.out.println(str);      }  }
复制代码


这个方法的作用是循环打印一个字符串集合,里面就用到了迭代器模式


java 语言已经完整地实现了迭代器模式,例如 List,Set,Map,而迭代器的作用就是把容器中的对象一个一个地遍历出来。

4.3 遍历时不建议删除元素

Iterator 遍历时不可以删除集合中的元素问题。在使用 Iterator 的时候禁止对所遍历的容器进行改变其大小结构的操作。


例如:在使用 Iterator 进行迭代时,如果对集合进行了 add、remove 操作就会出现 ConcurrentModificationException 异常。


在你迭代之前,迭代器已经被通过 list.iterator()创建出来了,如果在迭代的过程中,又对 list 进行了改变其容器大小的操作,那么 Java 就会给出异常。


因为此时 Iterator 对象已经无法主动同步 list 做出的改变,Java 会认为你做出这样的操作是线程不安全的,就会给出善意的提醒(抛出 ConcurrentModificationException 异常)


List<String> list = new ArrayList<String>();list.add("张三1");list.add("张三2");list.add("张三3");list.add("张三4");//……list.add("张三N");
//使用迭代器遍历ArrayList集合Iterator<String> listIt = list.iterator();while(listIt.hasNext()){ Object obj = listIt.next(); if(obj.equals("张三100")){ list.remove(obj); }}
复制代码


看一下 ArrayList 中的源码,看看迭代器中 next 和 remove 的实现方法代码。


public Iterator<E> iterator() {    return new Itr();}
/** * An optimized version of AbstractList.Itr */private class Itr implements Iterator<E> { // Android-changed: Add "limit" field to detect end of iteration. // The "limit" of this iterator. This is the size of the list at the time the // iterator was created. Adding & removing elements will invalidate the iteration // anyway (and cause next() to throw) so saving this value will guarantee that the // value of hasNext() remains stable and won't flap between true and false when elements // are added and removed from the list. protected int limit = ArrayList.this.size;
int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount;
public boolean hasNext() { return cursor < limit; }
@SuppressWarnings("unchecked") public E next() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); int i = cursor; if (i >= limit) 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(); if (modCount != expectedModCount) throw new ConcurrentModificationException();
try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; limit--; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
@Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1;
if (modCount != expectedModCount) throw new ConcurrentModificationException(); }}
复制代码


查看源码发现原来检查并抛出异常的是 checkForComodification() 方法。在 ArrayList 中 modCount 是当前集合的版本号,每次修改(增、删)集合都会加 1;expectedModCount 是当前迭代器的版本号,在迭代器实例化时初始化为 modCount。


我们看到在 checkForComodification()方法中就是在验证 modCount 的值和 expectedModCount 的值是否相等,所以当你在调用了 ArrayList.add()或者 ArrayList.remove()时,只更新了 modCount 的状态,而迭代器中的 expectedModCount 未同步,因此才会导致再次调用 Iterator.next()方法时抛出异常。


但是为什么使用 Iterator.remove()就没有问题呢?通过源码的第 32 行发现,在 Iterator 的 remove()中同步了 expectedModCount 的值,所以当你下次再调用 next()的时候,检查不会抛出异常。


使用该机制的主要目的是为了实现 ArrayList 中的快速失败机制(fail-fast),在 Java 集合中较大一部分集合是存在快速失败机制的。


快速失败机制产生的条件:当多个线程对 Collection 进行操作时,若其中某一个线程通过 Iterator 遍历集合时,该集合的内容被其他线程所改变,则会抛出 ConcurrentModificationException 异常


所以要保证在使用 Iterator 遍历集合的时候不出错误,就应该保证在遍历集合的过程中不会对集合产生结构上的修改。

4.4 for 循环 VS 迭代器

for 循环与迭代器的对比,效率上各有各的优势:


  1. ArrayList 对随机访问比较快,而 for 循环中使用的 get()方法,采用的即是随机访问的方法,因此在 ArrayList 里 for 循环快。

  2. LinkedList 则是顺序访问比较快,Iterator 中的 next()方法采用的是顺序访问方法,因此在 LinkedList 里使用 Iterator 较快。

4.5 迭代器原理介绍

迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。


它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。


迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。


迭代器是用来遍历容器的


所以,一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。

4.6 迭代器模式优势

一般来讲,遍历集合数据有三种方法:for 循环、foreach 循环、iterator 迭代器。对于这三种方式,我拿 Java 语言来举例说明一下。具体的代码如下所示:


List<String> names = new ArrayList<>();names.add("xzg");names.add("wang");names.add("zheng");
// 第一种遍历方式:for循环for (int i = 0; i < names.size(); i++) { System.out.print(names.get(i) + ",");}
// 第二种遍历方式:foreach循环for (String name : names) { System.out.print(name + ",")}
// 第三种遍历方式:迭代器遍历Iterator<String> iterator = names.iterator();while (iterator.hasNext()) { System.out.print(iterator.next() + ",");//Java中的迭代器接口是第二种定义方式,next()既移动游标又返回数据}
复制代码


实际上,foreach 循环只是一个语法糖而已,底层是基于迭代器来实现的。也就是说,上面代码中的第二种遍历方式(foreach 循环代码)的底层实现,就是第三种遍历方式(迭代器遍历代码)。这两种遍历方式可以看作同一种遍历方式,也就是迭代器遍历方式。


从上面的代码来看,for 循环遍历方式比起迭代器遍历方式,代码看起来更加简洁。那我们为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢?原因有以下三个。


  1. 首先,对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。

  2. 其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响。

  3. 最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改。除此之外,添加新的遍历算法,我们只需要扩展新的迭代器类,也更符合开闭原则。

05.迭代器者模式分析

5.1 迭代器模式优点

  1. ①简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于 hash 表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。

  2. ②可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。

  3. ③封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。

5.2 迭代器模式缺点

  1. 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像 ArrayList,我们宁可愿意使用 for 循环和 get 方法来遍历集合。

5.3 适用环境

  1. 需要顺序访问集合对象的元素,但又不想暴露集合的底层实现。比如:有时我们希望从列表、栈、队列等数据结构中提取数据,但不希望直接操作这些数据结构的内部。

  2. 支持多种遍历方式。迭代器模式不仅仅能实现前向遍历,还可以实现其他方式的遍历,如双向遍历、逆序遍历等。

  3. 希望为不同的集合对象提供统一的遍历接口。无论是链表、数组,还是自定义的复杂数据结构,迭代器模式可以提供一种统一的遍历接口。

5.4 思考题考察

06.观察者模式总结

6.1 总结一下学习

迭代器模式,也叫游标模式。它用来遍历集合对象。这里说的“集合对象”,我们也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。


一个完整的迭代器模式,一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。


容器中需要定义 iterator() 方法,用来创建迭代器。迭代器接口中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。容器对象通过依赖注入传递到迭代器类中。

6.2 更多内容推荐


23 种设计模式


6.3 更多内容


用户头像

杨充

关注

还未添加个人签名 2018-07-30 加入

还未添加个人简介

评论

发布
暂无评论
16.迭代器模式设计思想_杨充_InfoQ写作社区