17. 23种设计模式之<迭代器模式>

220 阅读8分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

在日常工作中, 我们常用的设计模式不是单例模式, 不是策略模式, 不是工厂模式, 而是我们今天要研究的迭代器模式. 我们在编程的时候, 经常需要遍历一个集合里的各个对象, 通常我们是怎么做的呢? 将创建和遍历耦合在一块, 如下所示:

    List<Book> books = new ArrayList<>();
        for (Book book: books) {
            System.out.println("书名: " + book.getName() );
        }

来看看这段代码的问题.

  1. 如果有多处List, 那就需要多次使用for循环遍历
  2. 如果代码需要扩展或者更换迭代方式呢? 需要多次修改for循环里面的内容, 违背了"开闭原则"

解决这个问题的方法有很多种, 其中一种就是我们今天要研究的迭代器模式. 将对象的创建和遍历相分离. 对客户隐藏了内部细节, 功能单一, 所以符合“单一职责原则”, 提供接口, 可以自由扩展聚合类和迭代器类, 符合“开闭原则”. 下面就来详细研究 迭代器模式

一、什么是迭代器模式

迭代器(Iterator)模式:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式

我们可以在迭代器中自主定义需要的方法,如上一个, 下一个,第一个,最后一个,是否还有等方法。

实际上在Java里面, 已经为我们实现好了,那就是Java的Iterator类。很多编程语言也都有自己的迭代器类,所以在我们实现迭代器模式的时候创建一个实现迭代器Iterator接口的类即可。(也可以自定义接口类)

二、迭代器模式的结构

先来看迭代器的UML图:

从图中, 我们可以看出迭代器模式一共有5个重要的组成部分。

  1. 抽象迭代器接口(Iterator):在抽象迭代器中有两个非常总要的抽象方法,hasNext()和next(). 前者是用来判断是否还有下一个元素,后者是用来获取当前指针指向的元素, 同时让指针指向下一个元素。在java中, 已经有定义好的Iterator接口,我们可以直接使用
  2. 具体迭代器实现类(ConcreteIterator)实现了抽象迭代器接口
  3. 抽象聚合类接口(Aggregate):定义了抽象的聚合类,并且定义了一个创建迭代器对象的接口。还可以定义一些其他的方法。
  4. 具体聚合实现类(ConcreteAggregate):实现抽象聚合类,返回一个具体迭代器对象。实现了创建迭代器对象的接口。
  5. 客户端(client):主要是main方法模拟客户端调用。

下面来看看基础结构用法:

第一步: 定义抽象迭代器接口

package com.lxl.www.designPatterns.iteratePattern.book;

/**
 * 迭代器接口
 * @param <T>
 */
public interface Iterator<T> {
    boolean hasNext();

    T next();
}

这个接口可以自己定义, 也可以直接使用jdk提供的Iterator接口。

第二步: 具体迭代器实现类

package com.lxl.www.designPatterns.iteratePattern.book;

import java.util.List;

/**
 * 具体迭代器
 */
public class ConcreteIterator implements Iterator{

    /** 聚合抽象类 */
    Aggregate aggregate;

    /** 下标 */
    int index;

    public ConcreteIterator(Aggregate aggregate) {
        this.aggregate = aggregate;
        index = 0;
    }

    @Override
    public boolean hasNext() {
        if (index < aggregate.getLength()) {
            return true;
        }
        return false;
    }

    @Override
    public Object next() {
        Object obj = aggregate.getByIndex(index);
        index ++;
        return obj;
    }
}

在具体迭代器实现类中, 定义了一个聚合成员变量, 并实现了hasNext()和next()方法。

第三步: 抽象聚合类接口

package com.lxl.www.designPatterns.iteratePattern.book;

/**
 * 抽象聚合类
 * Aggregate 集合, 聚合的意思
 */
public interface Aggregate {

    public int getLength();
    
    public Object getByIndex(int index);
    /**
     * 循环遍历
     * @return
     */
    Iterator iterator();
}

抽象聚合接口中获取了Iterator迭代器对象。并定义了getLength()方法和getByIndex(int index)方法。

第四步: 具体聚合实现类

package com.lxl.www.designPatterns.iteratePattern.book;

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

/**
 * 具体的聚合实现类
 */
public class ConcreteAggregate implements Aggregate {

    /** 对象集合 */
    List<Object> objs = new ArrayList<>();

    public void add(Object obj){
        this.objs.add(obj);
    }

    @Override
    public int getLength() {
        return objs.size();
    }

    @Override
    public Object getByIndex(int index) {
        return objs.get(index);
    }

    @Override
    public Iterator iterator() {
        return new ConcreteIterator(this);
    }
}

具体的聚合实现类实现了Iterator接口。

第五步: client客户端调用

public class Client {
    public static void main(String[] args) {
        ConcreteAggregate bookShelf = new ConcreteAggregate();
        bookShelf.add(new Book("java语言"));
        bookShelf.add(new Book("c++语言"));
        bookShelf.add(new Book("php语言"));
        bookShelf.add(new Book("前端语言"));

        Iterator iterator = bookShelf.iterator();
        while (iterator.hasNext()) {
            System.out.println(((Book)iterator.next()).getName());
        }
    }
}

客户端调用,主导代码是获取Iterator对象并判断是否还有元素, 有的话取出来。

三、迭代器模式的实现案例

1. 案例1

首都图书馆有很多书架, 架子上有很多书。 我们想要将书按照在书架上摆放的顺序录入书名。书架有小说类书架,科幻类书架。下面我们来实现这个需求。

第一步: 定义一个书籍实体

package com.lxl.www.designPatterns.iteratePattern.book;

/**
 *
 */
public class Book {
    /**
     * 书名
     */
    public String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

第二步: 定义抽象迭代器类 这里使用的是jdk自带的Iterator

第三步: 定义具体的迭代器类

package com.lxl.www.designPatterns.iteratePattern.BookShelf;

import java.util.Iterator;

/**
 * 图书迭代器
 */
public class BookIterator implements Iterator<Book> {

    IBookShelf<Book> bookShelf;

    int index;

    public BookIterator(IBookShelf bookShelf) {
        this.bookShelf = bookShelf;
        index = 0;
    }
    @Override
    public boolean hasNext() {
        if (index < bookShelf.getLength()) {
            return true;
        }
        return false;
    }

    @Override
    public Book next() {
        Book book = bookShelf.getByIndex(index);
        index ++;
        return book;
    }
}

第四步: 定义抽象聚合接口

package com.lxl.www.designPatterns.iteratePattern.BookShelf;


import java.util.Iterator;

/**
 * 书架抽象类
 */
public interface IBookShelf<T> {

    T getByIndex(int index);

    int getLength();
    /**
     * 返回书籍列表的集合
     * @return
     */
    public Iterator<T> iterator();

    void add(T t);
}

第五步: 定义具体的聚合实现类

  1. 定义小说书架
package com.lxl.www.designPatterns.iteratePattern.BookShelf;


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

/**
 * 小说类书架
 */
public class NovelBookShelf implements IBookShelf<Book> {

    List<Book> novels = new ArrayList<>();

    @Override
    public void add(Book novle) {
        novels.add(novle);
    }

    @Override
    public Book getByIndex(int index) {
        if (index < novels.size()) {
            return novels.get(index);
        }
        return null;
    }

    @Override
    public int getLength() {
        return novels.size();
    }

    @Override
    public Iterator iterator() {
        return new BookIterator(this);
    }
}

  1. 定义科幻类书架
package com.lxl.www.designPatterns.iteratePattern.BookShelf;


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

/**
 * 科幻类书架
 */
public class ScienceFictionBookShelf implements IBookShelf<Book> {

    List<Book> scienceFiction = new ArrayList<>();

    @Override
    public void add(Book novle) {
        scienceFiction.add(novle);
    }

    @Override
    public Book getByIndex(int index) {
        if (index < scienceFiction.size()) {
            return scienceFiction.get(index);
        }
        return null;
    }

    @Override
    public int getLength() {
        return scienceFiction.size();
    }

    @Override
    public Iterator iterator() {
        return new BookIterator(this);
    }
}

第六步: 客户端

package com.lxl.www.designPatterns.iteratePattern.BookShelf;

import java.util.Iterator;

public class Client {
    public static void main(String[] args) {

        System.out.println("=========小说类书架=========");
        IBookShelf<Book> novels = new NovelBookShelf();
        novels.add(new Book("老人与海"));
        novels.add(new Book("哈利波特"));

        BookIterator iterator = (BookIterator)novels.iterator();
        while (iterator.hasNext()){
            Book book = iterator.next();
            System.out.println(book.getName());
        }


        System.out.println("=========科幻类书架=========");
        IBookShelf scienceFiction = new NovelBookShelf();
        scienceFiction.add(new Book("三体I"));
        scienceFiction.add(new Book("三体II"));
        scienceFiction.add(new Book("三体III"));

        BookIterator scienceFictionIterator = (BookIterator)novels.iterator();
        while (scienceFictionIterator.hasNext()){
            Book book = scienceFictionIterator.next();
            System.out.println(book.getName());
        }
    }
}

第七步: 运行结果

=========小说类书架=========
老人与海
哈利波特
=========科幻类书架=========
老人与海
哈利波特

小说和科幻类书架使用的是同一类迭代器, 所以只需要定义一种迭代器即可.

2. 案例2

在java中, 很多集合都是使用的迭代器模式. 例如: List集合, Set集合. 接下来我们来看看如何作为客户端调用List和Set.

public class Client {
    public static void main(String[] args) {
        // set迭代器
        Set<String> sets = new HashSet<>();
        java.util.Iterator<String> iterator1 = sets.iterator();
        while (iterator1.hasNext()) {
            Object next = iterator.next();
        }

        // list迭代器
        List<String> lists = new ArrayList<>();
        java.util.Iterator<String> iterator2 = lists.iterator();
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }

        // map迭代器
        Map<String, Object> maps = new HashMap<>();
        Set<String> strings = maps.keySet();
        java.util.Iterator<String> iterator3 = strings.iterator();
        while (iterator3.hasNext()) {
            System.out.println(iterator3.next());
        }
    }
}

下面就以ArrayList为例来看看ArrayList是如何实现迭代器模式的. 注: ArrayList中还有很多其他的逻辑, 我们暂且忽略,主要看迭代器模式的部分.

  1. 在jdk中定义了Iterator接口.
package java.util;

import java.util.function.Consumer;

/**
 * An iterator over a collection.  {@code Iterator} takes the place of
 * {@link Enumeration} in the Java Collections Framework.  Iterators
 * differ from enumerations in two ways:
 *
 * <ul>
 *      <li> Iterators allow the caller to remove elements from the
 *           underlying collection during the iteration with well-defined
 *           semantics.
 *      <li> Method names have been improved.
 * </ul>
 *
 * <p>This interface is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @param <E> the type of elements returned by this iterator
 *
 * @author  Josh Bloch
 * @see Collection
 * @see ListIterator
 * @see Iterable
 * @since 1.2
 */
public interface Iterator<E> {
    
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

   
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

这就是我们第一步的定义抽象迭代器接口 2. 在AbstractList中定义了抽象迭代器的实现类--具体的迭代器

在jdk中有这样一个Iterable接口, ArrayList最终实现了这个接口. 在接口中定义了一个抽象方法 Iterator iterator(); 然后在抽象类AbstractList中重写了Iterator iterator();这个方法, 如下图所示:

这个觉得迭代器返回的是Ite对象. 这个对象就是具体迭代器对象. Ite是一个子类, 来看看具体的代码实现

private class Itr implements Iterator<E> {
        int cursor = 0;
        int lastRet = -1;
        int expectedModCount = modCount;

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

        public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
           ....
        }

        final void checkForComodification() {
            ....
        }
    }

这是一个子类, 里面重写了hasNext()方法和next()方法, 并定义了一个指针, 因为是子类方法,可直接使用父类集合, 所以这里不需要在定义操作的集合类.

第三步: 定义抽象聚合类接口. AbstractList抽象类继承自List接口, 在List中定义了iterator()抽象方法

public interface List<E> extends Collection<E> {

    int size();

    boolean isEmpty();

  
    boolean contains(Object o);

    
    Iterator<E> iterator();

    ......
}

第四步: 定义聚合实现类 在AbstractList中, 实现了List接口, 并重写了iterator()方法. 这就是聚合的实现类了.

我们说研究ArrayList, 但好像一直围绕AbstractList研究, 其实ArrayList继承了AbstractList并重写了iterator()方法. 而且重新定义了迭代器实现类Ite. 我们来看看代码实现

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ......

    public Iterator<E> iterator() {
        return new Itr();
    }

    /**
     * An optimized version of AbstractList.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();
            }
        }

        @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;
            checkForComodification();
        }

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

以上就是ArrayList中迭代器设计模式的使用, Set也是如此.

四、迭代器模式的使用场景

1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。

五、迭代器模式的优缺点

优点

1、分离了对象的创建过程和遍历过程 2、支持以不同的方式遍历一个聚合对象。 3、迭代器简化了聚合类。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

缺点

由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

六、迭代器模式对六大原则的应用

  1. 单一职责原则: 迭代器模式分离了对象的创建和迭代, 各司其职, 符合单一职责原则
  2. 里式替换原则: 父类出现的地方都可以使用子类替换. 这里接口方法定义的都是抽象的, 符合.
  3. 接口隔离原则: 接口尽量小,不要出现胖接口, 只提供访问者需要的方法. 符合
  4. 依赖倒置原则: 面向抽象编程, 而不是面向具体, 符合
  5. 迪米特法则: 最少知识原则,一个对象应当对其他对象尽可能少的了解。符合
  6. 开闭原则: 对扩展开放, 对修改关闭: 符合