从零开始学习Java设计模式 | 行为型模式篇:迭代器模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 行为型模式篇:迭代器模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下行为型模式里面的第八个设计模式,即迭代器模式。

概述

相信大家对于迭代器并不会感到陌生,因为平时我们使用的还是比较多的,注意,在这儿我说的是使用。

在开发过程中我们会经常去遍历单列集合,既然要遍历,那么我们就可以选择使用迭代器去遍历了,而迭代器底层使用的就是迭代器模式。那么到底什么是迭代器模式呢?下面我们就来看看它的概念。

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

看完迭代器模式的概念,相信大家一时半会还无法理解,不过没关系,下面我给大家解释一下。

迭代器模式是说提供一个对象来顺序访问聚合对象中的一系列数据,而这其实指的就是遍历,这句话中的聚合对象,大家可以理解成是集合(或者容器),自然地,聚合对象中的一系列数据就是指该集合(或者容器)里面存储的元素了。那么这样遍历有什么作用呢?不暴露聚合对象的内部表示,也就是说我们并不需要去关注集合(或者容器)内部是如何去存储元素的。

以上就是迭代器模式的概念,其实说到底,就是提供了一种遍历的方式。

理解了迭代器模式的概念之后,接下来,我们就来看看迭代器模式的结构,也就是它里面所拥有的角色。

结构

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素(聚合元素,你可以理解成是容器里面存储的数据)以及创建迭代器对象的接口。注意了,该角色只是提供了这么一些抽象方法,而这些抽象方法是要由子类来实现的,这个子类就是具体聚合角色类。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含hasNext()、next()等方法,hasNext()方法是用于判断是否还有元素的,而next()方法是用于获取下一个元素的。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

下面我会通过一个具体的案例来让大家再深入认识一下以上迭代器模式的四个角色。

迭代器模式案例

接下来,按照惯例我们通过一个案例来让大家再去理解一下迭代器模式的概念,以及它里面所包含的角色。这个案例就是定义一个可以存储学生对象的容器对象,并将遍历该容器的功能交由迭代器实现。

分析

在该案例中,我们需要定义一个可以存储学生对象的容器对象,其实说到底就是让我们自定义一个集合,而这个集合里面存储的是学生对象。然后再将遍历该容器的功能交由迭代器实现,而这正好就要用到迭代器模式。

接下来,我们来看一下下面的这张类图。

可以看到,以上类图的最下面有一个学生类,即Student,该类的对象就是容器里面要存储的元素。至于该类里面的属性和方法,你想定义什么都行(这不,该类我们是像上面类图那样设计的),因为该类的对象就是往容器里面去存的,也就是说容器里面只能存储学生对象。

然后,我们来看一下以上类图的左侧部分。可以看到,上面有一个接口,名称是StudentIterator,翻译过来就是学生迭代器,很明显,该接口充当的就是迭代器模式里面的抽象迭代器角色。而且,该接口里面定义了两个规范方法,分别是:

  1. boolean hasNext():判断是否还有元素。
  2. Student next():获取下一个元素。注意,该方法的返回类型是Student类型,并不是特别通用,因为该方法只能获取学生对象。如果你想让该方法更通用,即能获取其他任意类型的对象,那么不妨用一下泛型。当然了,这儿我并没有这样去做啊,所以就不做过多的说明了。

再来看下面,可以看到下面有一个类,名称是StudentIteratorImpl,很明显,该类就是以上抽象迭代器接口的子实现类,那么它充当的就是迭代器模式里面的具体迭代器角色。而且,该类里面声明了两个成员变量,一个是List<Student>类型的成员变量,很显然,该成员变量里面存储的是学生对象。为什么要在该类里面声明这么一个类型的成员变量呢?这是因为到时候我们定义的容器里面存储的就是学生对象,当然了,使用的也是List集合来存储学生对象。此外,还有一个int类型的成员变量,该成员变量是用来记录遍历时的位置的。

大家也能看到,在该具体迭代器类中,我们还提供了一个有参的构造方法,它就是用于给List<Student>类型的成员变量赋值的。除此之外,该类还重写了父接口里面的两个抽象方法。

最后,我们来看一下以上类图的右侧部分。可以看到,上面有一个接口,名称是StudentAggregate,该接口充当的是迭代器模式里面的抽象聚合角色。而且,该接口里面定义了一些规范方法,分别是:

  1. void addStudent(Student student):往聚合对象里面添加学生对象。
  2. void removeStudent(Student student):从聚合对象里面删除学生对象。
  3. StudentIterator getStudentIterator():获取学生迭代器对象。

注意,以上只是一些规范方法,具体应该由具体的子类来实现。

再来看下面,可以看到下面有一个类,名称是StudentAggregateImpl,很明显,该类就是以上抽象聚合接口的子实现类,那么它充当的就是迭代器模式里面的具体聚合角色。而且,从上还可以看到,该类的成员位置处定义了一个List集合,而集合里面存储的正是学生对象。当然,我们现在是在设计,肯定是知道这个聚合对象里面的具体的实现的,然而,对于使用者来说,他到时候就不用去关注于底层是如何实现的了。除此之外,该类还重写了父接口里面的三个抽象方法。

至此,对于以上类图中所涉及到的类和接口,我们就一一分析清楚了。至于类和类,以及类和接口之间的关系,大家看上图便知道了。

实现

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即iterator,也即实现以上案例的具体代码我们是放在了该包下。

然后,创建学生类,这里我们就命名为Student了。

package com.meimeixia.pattern.iterator;

/**
 * 学生类
 * @author liayun
 * @create 2021-09-18 10:17
 */
public class Student {

    private String name; // 姓名
    private String num; // 学号

    public Student() {

    }

    public Student(String name, String num) {
        this.name = name;
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNum() {
        return num;
    }

    public void setNum(String num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\\'' +
                ", num='" + num + '\\'' +
                '}';
    }

}

接着,创建迭代器接口,这里我们不妨就命名为StudentIterator。

package com.meimeixia.pattern.iterator;

/**
 * 抽象迭代器角色接口
 * @author liayun
 * @create 2021-09-18 10:22
 */
public interface StudentIterator {

    /**
     * 判断是否还有元素
     */
    boolean hasNext();

    /**
     * 获取下一个元素
     *
     * 因为我们这儿是专门用来遍历学生对象的,所以该方法的返回值类型我们就设置为了Student。
     * 当然,如果你想让该方法更通用,那么不妨使用一下泛型,只是在这里我并没有这样去做!
     */
    Student next();

}

紧接着,创建具体迭代器类,这里我们不妨就命名为StudentIteratorImpl。

package com.meimeixia.pattern.iterator;

import java.util.List;

/**
 * 具体迭代器角色类
 * @author liayun
 * @create 2021-09-18 10:31
 */
public class StudentIteratorImpl implements StudentIterator {

    /*
     * 声明一个List<Student>类型的成员变量,这样,该成员变量里面存储的就是学生对象了。
     *
     * 为什么要在该类里面声明这么一个类型的成员变量呢?这是因为在具体聚合类中,本质上存储
     * 元素的就是List集合,所以在这里我们也声明一个List<Student>类型的成员变量。
     */
    private List<Student> list;

    /*
     * 用来记录遍历时的位置,也就是说我们遍历到了第几个元素
     */
    private int position = 0;

    public StudentIteratorImpl(List<Student> list) {
        this.list = list;
    }

    @Override
    public boolean hasNext() {
        return position < list.size();
    }

    @Override
    public Student next() {
        // 从集合中获取指定位置的元素
        Student currentStudent = list.get(position);
        // 获取到当前学生对象之后,千万别忘了让位置发生一个变化,因为当前位置的元素我们已经获取了
        position++;
        return currentStudent;
    }

}

抽象迭代器接口与具体迭代器类创建完毕之后,接下来我们就要创建抽象聚合接口与具体聚合类了。这里我们先创建抽象聚合接口,名称不妨就叫作StudentAggregate。

package com.meimeixia.pattern.iterator;

/**
 * 抽象聚合角色接口
 * @author liayun
 * @create 2021-09-18 10:39
 */
public interface StudentAggregate {

    // 添加学生功能
    void addStudent(Student stu);

    // 删除学生功能
    void removeStudent(Student stu);

    // 获取迭代器对象功能
    StudentIterator getStudentIterator();

}

很显然,这里我们就要创建具体聚合类了,名称不妨就叫作StudentAggregateImpl。

package com.meimeixia.pattern.iterator;

import java.util.ArrayList;
import java.util.List;

/**
 * 具体聚合角色类
 * @author liayun
 * @create 2021-09-18 10:46
 */
public class StudentAggregateImpl implements StudentAggregate {

    /*
     * 由于该类的对象是一个聚合对象,所以我们应该先创建一个能存储元素的集合。
     */
    private List<Student> list = new ArrayList<Student>(); // 学生列表

    @Override
    public void addStudent(Student stu) {
        list.add(stu);
    }

    @Override
    public void removeStudent(Student stu) {
        list.remove(stu);
    }

    // 获取迭代器对象
    @Override
    public StudentIterator getStudentIterator() {
        /*
         * 注意,创建迭代器对象时,我们是把当前聚合对象里面的List集合作为一个参数进行了传递,
         * 这样,迭代器对象里面的List集合就和当前聚合对象里面的List集合保持一致了,所以,当
         * 我们使用迭代器遍历元素时,实际上我们获取的就是聚合对象里面的容器(List集合)中的元素!
         */
        return new StudentIteratorImpl(list);
    }

}

最后,创建客户端类用于测试。

package com.meimeixia.pattern.iterator;

/**
 * @author liayun
 * @create 2021-09-18 11:02
 */
public class Client {
    public static void main(String[] args) {
        // 创建聚合对象
        StudentAggregateImpl aggregate = new StudentAggregateImpl();
        // 添加元素
        aggregate.addStudent(new Student("张三", "001"));
        aggregate.addStudent(new Student("李四", "002"));
        aggregate.addStudent(new Student("王五", "003"));
        aggregate.addStudent(new Student("赵六", "004"));

        // 遍历聚合对象
        // 1. 获取迭代器对象
        StudentIterator iterator = aggregate.getStudentIterator();
        // 2. 遍历
        while (iterator.hasNext()) {
            // 3. 获取元素
            Student student = iterator.next();
            System.out.println(student.toString());
        }
    }
}

此时,运行以上客户端类,打印结果如下图所示,可以看到确实是按照学生对象添加的顺序将添加进去的学生列表给打印出来了。

以上就是迭代器模式在开发中的一个具体应用。当然,相信大家对于以上客户端类的代码应该是无比熟悉的,因为使用迭代器对单列集合进行遍历的代码和上面是一模一样的,只是这里我给大家主要讲的是设计的思想。

迭代器模式的优缺点以及使用场景

接下来,我们就来看一看迭代器模式的优缺点以及使用场景。

优缺点

优点

关于迭代器模式的优点,这里我总结了三点。

  1. 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。

    假设我们自己要去定义一个迭代器的子类的话,那么应该怎么办呢?首先,肯定是得让它去实现StudentIterator接口,然后还需要稍微修改一下具体聚合类的代码,即在获取迭代器对象的方法中返回我们自己定义的迭代器对象。

  2. 迭代器简化了聚合类。由于引入了迭代器,所以在原有的聚合对象中不再需要自行提供数据遍历等方法,这样就可以简化聚合类的设计了。

  3. 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,所以满足了"开闭原则"的要求。

缺点

增加了类的个数,这在一定程度上增加了系统的复杂性。

其实,对于我们来说,使用迭代器模式所带来的这个缺点也是能够接受的,无非就是多引入了抽象迭代器角色和具体迭代器角色这俩角色。

使用场景

只要出现如下几个场景,我们就可以去考虑一下能不能使用迭代器模式了。

  • 当需要为聚合对象提供多种遍历方式时。此时我们就可以考虑使用迭代器模式了,因为迭代器模式说到底主要就是给聚合对象提供遍历方式。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

其实,以上三种使用场景归根结底无非就是说迭代器模式为聚合对象提供了一种遍历方式,所以,只要牵扯到和遍历相关的操作,我们就可以考虑使用迭代器模式了。

迭代器模式在JDK源码中的应用

接下来,我们就来看一下迭代器模式在JDK源码中的具体应用。

迭代器模式在Java的很多集合类中都被广泛应用到了,只是这里所说的集合特指单列集合。接下来,我们就来看看Java源码中是如何使用迭代器模式的。

先来看下面这一段代码。

List<String> list = new ArrayList<String>();
Iterator<String> iterator = list.iterator(); // list.iterator()方法返回的肯定是Iterator接口的子实现类对象。那到底是哪个子实现类的对象呢?别着急,后面我就会讲到。
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

看完这段代码是不是很熟悉啊,与我们上面案例里面写的代码基本类似。

既然单列集合都使用到了迭代器,那么接下来我们就以ArrayList举例来说明它是如何使用迭代器模式的。

首先,我们先来分析一下ArrayList使用的迭代器模式里面各个角色所对应的类和接口分别是哪些。

  • List:抽象聚合接口
  • ArrayList:具体聚合类
  • Iterator:抽象迭代器接口
  • list.iterator():ArrayList类里面的iterator方法返回的是实现了Iterator接口的具体迭代器对象。

问题来了,究竟具体迭代器对象是Iterator接口的哪个子实现类的对象呢?下面我们就来具体看看ArrayList类的源码实现,从源码中来寻找答案。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    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];
        }

        ......
    }

}

这里,我只是摘出了ArrayList类中我们要分析的代码。可以看到,ArrayList类实现了List接口,并重写了iterator方法,当然该方法的返回值类型是Iterator,大家要注意的是它是一个接口,所以我们就知道了iterator方法返回的肯定是Iterator接口的子实现类对象。而且,从iterator方法内部我们也知道了这个子实现类的名称就叫Itr,那该子实现类是在哪定义的呢?

其实,该子实现类就定义在iterator方法下面,也就是说Itr是ArrayList里面定义的私有成员内部类,并且它还实现了Iterator接口,重写了hasNext和next这俩方法,所以,Itr就是具体迭代器类。

分析完ArrayList使用的迭代器模式里面各个角色所对应的类和接口分别是哪些之后,你就知道迭代器模式在JDK源码中的具体应用了。

最后,我总结一点:当我们在使用Java开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现java.lang.Iterable接口并实现其中的iterator()方法使其返回一个java.util.Iterator接口的子实现类就可以了

以上是关于从零开始学习Java设计模式 | 行为型模式篇:迭代器模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式