Iteratorパターン[Java]

GoFデザインパターンについて学んでます.

今回は「Iterator」パターンについて学びます.

Iterator Pattern って何

Java言語では配列の要素を全て表示するために for文で以下のように書きます.

for(int i = 0; i < array.length; i++){
    System.out.println(array[i]);
}

for文のi++でiを1ずつ増加させると, 現在注目している要素を「次」「その次」…
のように進めていることはお分かりになると思います.
iを進めて,配列の要素全体を最初から順番に走査していることになります.

ここで使われている変数iの働きを抽象化し,一般化したものを
デザインパターンではIteratorパターンと呼ばれます.

集合体を順番に指示していき,全体を走査していく処理を行うためのパターンです.
Iterateとは「繰り返す」という意味です. 日本語では反復子と呼ばれることがあります.

サンプルプログラムのクラス図

クラスとインタフェース一覧は以下のようになります. f:id:xrdnk:20200106212617p:plain

名前 解説
Aggregate 集合体を表すインタフェース
Iterator 数え上げて走査を行うインタフェース
Book 本クラス
BookShelf 本棚クラス
BookShelfIterator 本棚を走査するクラス
Main 動作テスト用クラス

Aggregateインタフェース

package iterator;

/**
 * 集合体を表すインターフェイス
 *
 */
public interface Aggregate {

    // 集合体を数え上げる
    public abstract Iterator iterator();
}

Aggregateとは「集合体」を意味します.
配列やCollectionのように「何かが沢山集まっているコンテナ」だと思ってください.
iterator抽象メソッドを宣言しています.

Iteratorインタフェース

package iterator;

/**
 * 数え上げて走査を行うインターフェイス
 *
 */
public interface Iterator {

    // 次の要素が存在するかどうか
    public abstract boolean hasNext();

    // 次の要素
    public abstract Object next();
}

宣言されているメソッドはboolean hasNext()とObject next()です.

hasNext()
hasNext()は次の要素が存在すればtrue,存在しなければfalseになります.
hasNext()はループの終了条件で使われます.
「最後」が間違え易く,最後の要素を得る前はtrue,最後の要素を得た後はfalseになることに注意.

next()
next()は再度呼び出されたときにちゃんと次の要素を返すように,
内部状態を次に進めておく裏の仕事があります.
ぶっちゃけこのnext(),わかりにくい.戻り値は「現在の要素」です.
「現在の値を返して,次の位置に進める」意味です.
returnCurrentElementAndAdvanceToNextPositionでイメージするといいでしょう.

Bookクラス

package iterator;

/**
 * 本クラス
 *
 */
public class Book {

    private String name;

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

    public String getName() {
        return name;
    }
}

BookShelfクラス

package iterator;

/**
 * 本棚を表現するクラス
 *
 */
public class BookShelf implements Aggregate {

    private Book[] books;

    // 最後の要素
    private int last = 0;

    // コンストラクタ
    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }

    // index番の本を取得
    public Book getBookAt(int index) {
        return books[index];
    }

    // 本を追加
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }

    // 本の数を取得
    public int getLength() {
        return last;
    }

    // 本棚クラスに対応するイテレーターを生成
    public Iterator iterator() {
        return new BookShelfIterator(this);
    }
}

集合体として扱うためにAggregateインタフェースを実装しています.

BookShelfIteratorクラス

package iterator;

/**
 * 本棚をスキャンするクラス.
 *
 */
public class BookShelfIterator implements Iterator {

    private BookShelf bookShelf;

    private int index;

    // コンストラクタ
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }

    public boolean hasNext() {
        if(index < bookShelf.getLength()) {
            return true;
        }else {
            return false;
        }
    }

    public Object next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}

イテレータとして扱うため,Iteratorインタフェースを実装しています.
hasNext()で「次の本」があるかどうか調べて,あるならばtrue,なければfalseを返します.
next()は現在注目している本(Bookインスタンス)を返し,さらに「次」へ進めます.
まず,戻り値として返すべき本をbookという変数に入れて,indexを次に進めます.

Mainクラス

package iterator;

/**
 * 動作テスト用のクラス
 *
 */
public class Main {

    public static void main(String[] args) {

        // 本棚インスタンス生成
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("鬼滅の刃"));
        bookShelf.appendBook(new Book("ワンピース"));
        bookShelf.appendBook(new Book("進撃の巨人"));
        bookShelf.appendBook(new Book("呪術廻戦"));

        // イテレータ生成
        Iterator it = bookShelf.iterator();

        // 次の本がある限り,本を数え上げる
        while(it.hasNext()) {
            Book book = (Book)it.next();
            System.out.println(book.getName());
        }
    }
}

出力はこのようになります.

鬼滅の刃
ワンピース
進撃の巨人
呪術廻戦

ぶっちゃけfor文でよくない?

Iteratorパターンを用いることで実装と切り離して数え上げを行うことができるから.

       while(it.hasNext()) {
            Book book = (Book)it.next();
            System.out.println(book.getName());

こちらのコードはBookShelfの実装,データ構造には依存しません.
例えばBookShelfが配列じゃなくList型に変更しても上のループは変えなくても動作します.

また,BookShelfのサイズを知らなくても, Iteratorなら「次があるかどうかのhasNext()」と
「次の要素を得るのnext()」の操作さえあれば処理できる.
また,HashSetクラスのような順番を持たないCollectionでも使えるのが強みかと.

じゃあ拡張for文でいいじゃんw

まあJava SE 5.0では拡張for文(C#でいうforeach文)が導入されたので,その通りなんですが.
でも拡張for文の内部の働きはIteratorと同じ働きだったりします.

CollectionクラスはIterableインタフェースを実装しているためです.
Iterableとは「繰り返し可能,反復可能」という意味です.
集合体は繰り返し要素を取得できますよね.
AggregateインタフェースはIterableインタフェースと言ってもよいじゃないんでしょうか.
Iterableインタフェースはiterator()メソッドを持ちます.
拡張for文に渡すと、拡張for文の内部でiterator()メソッドが呼び出されております.

またjava.util.IteratorにはhasNext()とnext()のほかにremove()というメソッドがあります.
これはイテレーション中にコレクションの要素を削除するメソッドです.
拡張for文にはないため,ループ中に削除したい要素があればこっちの勝ちです.

参考

Iterable (Java Platform SE 8)

Iterator (Java Platform SE 8)

Javaのイテレータ(拡張for構文) | 酒と涙とRubyとRailsと

C#(.NET Framework)では…

上のAggregate/Iterable(C#の記法ではIAggregate/IIterable)インタフェースと
Iterator(C#の記法ではIIterator)インタフェースは, C#では各々IEnumerable,IEnumeratorに相当します.
ん…?この2つって確かUnityのコルーチンでよく見る戻り値のアレじゃね…?
ということで次回はここらへん掘り下げるかもしれません.