迭代器模式

迭代器模式

案例

最近张三在书店上班,老板有着两家书店。一天老板让他把书店 A 和书店 B 中存放的书籍罗列出来。他来到两个书店发现两个书店存放书籍的格式是不一样的,其中书店 A 内部采用数组的形式,书店 B 采用链表的形式。现在他需要根据不同的格式进行遍历罗列,程序类似下面这样:

1.首先定义书籍实体类

/**
 * 书籍类
 */
public class Book {
    private String bookName;

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

    @Override
    public String toString() {
        return "Book{" +
                "bookName='" + bookName + '\'' +
                '}';
    }
}

2.书店 A:

/**
 * A 书店内部使用数组存储书籍
 */
public class BookStoreA {
    private Book[] books;

    private int index;

    public BookStoreA() {
        books = new Book[10];
    }

    // 添加书籍
    public void addBook(Book book) {
        if (index == books.length) {
            throw new IndexOutOfBoundsException("容量不足");
        }
        books[index] = book;
        index++;
    }

    public Book[] getBooks() {
        return books;
    }

    public int getIndex() {
        return index;
    }
}

3.书店 B:

/**
 * B 书店内部使用链表存储书籍
 */
public class BookStoreB {
    // 记录第一个节点
    private Node<Book> first;
    // 记录最后一个节点
    private Node<Book> last;

    public void addBook(Book book) {
        Node<Book> node = new Node<>(book, null);
        if (first == null) {
            first = last = node;
        } else {
            last.next = node;
            last = node;
        }
    }

    /**
     * 实际存放数据及结构的内部类
     * @param <Book>
     */
    static class Node<Book> {
        // 存放数据
        Book book;
        // 存放/指向下一个节点
        Node<Book> next;

        Node(Book book, Node<Book> next) {
            this.book = book;
            this.next = next;
        }
    }

    public Node<Book> getFirst() {
        return first;
    }
}

4.罗列书籍:

public class Main {
    public static void main(String[] args) {
        BookStoreA bookStoreA = new BookStoreA();
        bookStoreA.addBook(new Book("A Book"));
        bookStoreA.addBook(new Book("B Book"));
        bookStoreA.addBook(new Book("C Book"));
        bookStoreA.addBook(new Book("D Book"));
        // 遍历书籍
        Book[] books = bookStoreA.getBooks();
        for (int i = 0; i < bookStoreA.getIndex(); i++) {
            System.out.println(books[i]);
        }

        System.out.println("--------------------------");

        BookStoreB bookStoreB = new BookStoreB();
        bookStoreB.addBook(new Book("A Book"));
        bookStoreB.addBook(new Book("B Book"));
        bookStoreB.addBook(new Book("C Book"));
        bookStoreB.addBook(new Book("D Book"));
        // 遍历书籍
        BookStoreB.Node<Book> first = bookStoreB.getFirst();
        while (first != null) {
            System.out.println(first.book);
            first = first.next;
        }
    }
}

5.罗列效果:

Book{bookName='A Book'}
Book{bookName='B Book'}
Book{bookName='C Book'}
Book{bookName='D Book'}
--------------------------
Book{bookName='A Book'}
Book{bookName='B Book'}
Book{bookName='C Book'}
Book{bookName='D Book'}

可以看到虽然两个书店存放书籍的数据格式不同,张三还是能够对不同的数据格式进行遍历。但是,如果还有一家店存放书籍的格式变化了,或新增了一家书店同时存放书籍的方式也同样变化了,那么张三就需要针对变化后的数据格式重新变换其他遍历方式。这一问题在设计模式中对应的就是迭代器模式,它解决了对不同数据结构遍历方式的问题。

模式介绍

迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。

角色构成

  • Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。
  • ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。
  • Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。
  • ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。

UML类图

iterator

在迭代器模式中,引入了迭代器类,它定义了对聚合类进行访问的接口,使得对每一种聚合类都可以通过的具体的迭代器进行遍历。下面就对上面的案例进行改造。

代码改造

1.首先是书籍类,与上面代码相同。

2.抽象聚合类:

/**
 * 抽象聚合类角色
 */
public interface Aggregate {
    Iterator createIterator();
}

3.抽象迭代器:

/**
 * 抽象迭代器角色
 */
public interface Iterator {
    // 判断是否存在下一个元素
    boolean hasNext();
    // 获取下一个元素
    Object next();
}

4.具体聚合类,书店 A:

/**
 * A 书店内部使用数组存储书籍
 */
public class BookStoreA implements Aggregate {
    // ...
	// 其余代码与案例相同
    @Override
    public Iterator createIterator() {
        return new BookStoreAIterator(this);
    }
}

具体聚合类,书店 B:

/**
 * B 书店内部使用链表存储书籍
 */
public class BookStoreB implements Aggregate {
    // ...
	// 其余代码与案例相同
    @Override
    public Iterator createIterator() {
        return new BookStoreBIterator(this);
    }

}

5.具体迭代器:书店 A 迭代器:

/**
 * 具体迭代器,针对数组形式重写 hasNext()、next() 等方法
 */
public class BookStoreAIterator implements Iterator {
    private BookStoreA bookStoreA;
    private int index;

    public BookStoreAIterator(BookStoreA bookStoreA) {
        this.bookStoreA = bookStoreA;
    }

    @Override
    public boolean hasNext() {
        return this.index < bookStoreA.getIndex();
    }

    @Override
    public Book next() {
        return bookStoreA.getBooks()[this.index++];
    }
}

书店 B 迭代器:

/**
 * 具体迭代器,针对链表形式重写 hasNext()、next() 等方法
 */
public class BookStoreBIterator implements Iterator {
    private BookStoreB bookStoreB;
    private BookStoreB.Node<Book> node;

    public BookStoreBIterator(BookStoreB bookStoreB) {
        this.bookStoreB = bookStoreB;
        this.node = new BookStoreB.Node<>(null, bookStoreB.getFirst());
    }

    @Override
    public boolean hasNext() {
        boolean hasNext = this.node.next != null;
        this.node = this.node.next;
        return hasNext;
    }

    @Override
    public Book next() {
        return node.book;
    }
}

6.测试类:

public class Main {
    public static void main(String[] args) {
        BookStoreA bookStoreA = new BookStoreA();
        bookStoreA.addBook(new Book("A Book"));
        bookStoreA.addBook(new Book("B Book"));
        bookStoreA.addBook(new Book("C Book"));
        bookStoreA.addBook(new Book("D Book"));

        // 使用迭代器进行统一遍历
        Iterator bookStoreAIterator = bookStoreA.createIterator();
        while (bookStoreAIterator.hasNext()) {
            System.out.println(bookStoreAIterator.next());
        }
        System.out.println("---------------------------");
        BookStoreB bookStoreB = new BookStoreB();
        bookStoreB.addBook(new Book("A Book"));
        bookStoreB.addBook(new Book("B Book"));
        bookStoreB.addBook(new Book("C Book"));
        bookStoreB.addBook(new Book("D Book"));

        // 使用迭代器进行统一遍历
        Iterator bookStoreBIterator = bookStoreB.createIterator();
        while (bookStoreBIterator.hasNext()) {
            System.out.println(bookStoreBIterator.next());
        }
    }
}

7.测试结果:

Book{bookName='A Book'}
Book{bookName='B Book'}
Book{bookName='C Book'}
Book{bookName='D Book'}
---------------------------
Book{bookName='A Book'}
Book{bookName='B Book'}
Book{bookName='C Book'}
Book{bookName='D Book'}

通过引入迭代器接口,然后针对不同的数据结构存储方式的书店提供不同的具体迭代器类,就可以通过统一的访问方式对其进行遍历。如果新增一种其他形式的存储方式,只用新增一个具体的迭代器类,而使用方不需要更改访问方式,同样可以对其进行访问,符合“开闭原则”。

模式应用

迭代器模式 Java 中最常见的应用就是集合框架中,为了让开发人员更加方便地遍历集合对象,实现了Collection(接口中定义了Iterator<E> iterator()方法,待子类具体实现)接口的类,都提供了迭代器来统一的访问集合内部的元素,比如ArrayList实现类在遍历元素时可以这样遍历:

public class Main {
    public static void main(String[] args) {
        List<Book> bookList = new ArrayList<>();
        bookList.add(new Book("A Book"));
        bookList.add(new Book("B Book"));
        bookList.add(new Book("C Book"));
        bookList.add(new Book("D Book"));

        // 迭代器遍历
        Iterator<Book> bookIterator = bookList.iterator();
        while (bookIterator.hasNext()) {
            System.out.println(bookIterator.next());
        }
    }
}

可以看到这里遍历集合使用的是迭代器Iterator对集合元素进行访问的,而迭代器是从它自身提供的方法iterator获取的。itertor():

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

这个方法返回迭代器接口的具体实现类实例,首先是Iterator接口:

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

可以看到它定义了访问元素的接口方法,然后是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() {
        // ...
    }

    public void remove() {
        // ...
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        // ...
    }
    // ...
}

Itr它实现了Iterator接口,实现了访问ArrayList内部数据的方法。与案例改造代码中不同的地方就是,它是ArrayList中私有的内部类,而改造代码时定义的是一个公共的类。JDK 这样设计的想法我觉得是Itr本来就是用于访问ArrayList的内部元素的,把它设计为内部类能更好的访问外部内的数据,同时设置为私有的使得外部访问不到Itr类。下面是这几个类之间的类图关系:

iterator-jdk-collection

总结

1.主要优点

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。

2.主要缺点

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。

3.适用场景

  • 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。
  • 需要为一个聚合对象提供多种遍历方式。
  • 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。

参考资料

  • 大话设计模式
  • 设计模式Java版本-刘伟

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/iterator
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/14151798.html

posted @ 2020-12-17 20:30  Phoegel  阅读(79)  评论(0编辑  收藏  举报