広告

Java : Iterator パターン (図解/デザインパターン)

Iterator パターンとは、GoF によって定義されたデザインパターンの1つです。
コンテナの要素を、共通のインタフェースで順々にアクセスします。

本記事では、Iterator パターンについてコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

Iterator パターン(イテレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。コンテナオブジェクトの要素を列挙する手段を独立させることによって、コンテナの内部仕様に依存しない反復子を提供することを目的とする。

Iterator パターンとは、

  • ある要素の集まりがあり
  • そのすべての要素に順々にアクセスする

という目的のための設計です。

ここで、本記事で使う用語について定義します。

用語 意味
要素 (element) 値やオブジェクト
コンテナ 要素の集まり
イテレータ コンテナの要素に順次アクセスするための仕組み、またはオブジェクト。
反復子ともいいます。

コンテナ


Java の標準API を例にすると、List はコンテナです。
List に対しては、各要素にアクセスするためにインデックスが使えます。

次の例では、3つの要素を持つ List に対して、インデックスの 0 ~ 2 を使って要素にアクセスします。

final var list = List.of("aaa", "bbb", "ccc");

for (int i = 0; i < list.size(); i++) {
    final var element = list.get(i);
    System.out.println("index = " + i + " : " + element);
}

// 結果
// ↓
//index = 0 : aaa
//index = 1 : bbb
//index = 2 : ccc

もう1つのコンテナの例として Set があります。
Set は List と違ってインデックスは使えません。要素が存在するかどうかを contains メソッドで判定します。

final var set = Set.of("aaa", "bbb", "ccc");

System.out.println(set.contains("aaa")); // true
System.out.println(set.contains("bbb")); // true
System.out.println(set.contains("ccc")); // true
System.out.println(set.contains("XXX")); // false

このように、コンテナの仕様によって要素へのアクセス方法は異なります。

しかし、Iterator パターンを使えば

  • 共通のインタフェース

で、すべての要素に順々にアクセスすることができます。

実は、Java 標準API の List と Set は、Iterator パターンが適用されています。
次の例では、反復子(Iterator) を使って、List と Set の要素をすべて表示させます。

// コンテナの要素をすべて表示するメソッドです。
public void print(Iterable<String> container) {
    final var it = container.iterator();
    while (it.hasNext()) {
        final var element = it.next();
        System.out.println("element = " + element);
    }
}
final var list = List.of("aaa", "bbb", "ccc");
print(list);

// 結果
// ↓
//element = aaa
//element = bbb
//element = ccc

final var set = Set.of("XXX", "YYY", "ZZZ");
print(set);

// 結果
// ↓
//element = XXX
//element = YYY
//element = ZZZ

このように、List でも Set でも、同じ print メソッドを使って要素にアクセスできるわけですね。


ノート

  • Java で Iterator パターンを1から実装することは少ないと思います。
    というのも、Java の標準 API には、Iterator パターン用の API がすでに用意されているからです。

  • もし Java で Iterator パターンを作りたいかたは、後述する「Java の標準API を利用する例」もご参照ください。



クラス図

クラス図

上記が、Iterator パターンの基本的なクラス図になります。

上の2つは インタフェース です。

  • Aggregate : イテレータ(反復子) を生成
  • Iterator : 要素に順次アクセス

という役割を持ちます。

インタフェースを主眼としたシーケンス図は次のようになります。

シーケンス図


下の2つは、Aggregate と Iterator を実装した 具体的な クラスです。

  • ConcreteAggregate : Aggregate の実装。実体は コンテナ です。
  • ConcreteIterator : Iterator の実装。

必要に応じて、ConcreteAggregateA, ConcreateAggregateB … のように複数の実装を作ることもあります。

コード例

先述した Iterator パターンのクラス図ですが、コード例を作るにあたり、もう少し具体的な名前に変更します。

クラス図

まずは上2つのインタフェースです。

public interface Aggregate {

    Iterator iterator();
}
public interface Iterator {

    String next();

    boolean hasNext();
}

今回のコード例では、要素の型は String としています。
Iterator.next メソッドの戻り値のところですね。

もし汎用的な型にしたい場合は、ジェネリクス(総称型) を使うのもよいでしょう。

public interface Iterator<T> {

    T next();

    boolean hasNext();
}

次に、下2つのクラスです。

  • Container : コンテナ
  • ContainerIterator : コンテナ用のイテレータ

Container クラスですが、まずは Iterator は考えずに作成してみます。

public class Container {

    private final String[] elements = {"aaa", "bbb", "ccc"};

    public String get(int index) {
        return elements[index];
    }

    public int size() {
        return elements.length;
    }
}

Container クラスは、簡単なコード例とするために、

  • "aaa"
  • "bbb"
  • "ccc"

という3つの要素を固定で持たせます。

要素へのアクセスは get メソッドで行います。標準API の List に似た使い方ですね。

final var container = new Container();

for (int i = 0; i < container.size(); i++) {
    final var element = container.get(i);
    System.out.println("index = " + i + " : " + element);
}

// 結果
// ↓
//index = 0 : aaa
//index = 1 : bbb
//index = 2 : ccc

それでは、この Container クラスに Iterator パターンを適用してみましょう。

Container クラスに Aggregate インタフェースを実装します。
オーバーライドする iterator メソッドでは、ContainerIterator クラスを生成して返します。

public class Container implements Aggregate {
                       ^^^^^^^^^^^^^^^^^^^^ <---- 実装

    private final String[] elements = {"aaa", "bbb", "ccc"};

    public String get(int index) {
        return elements[index];
    }

    public int size() {
        return elements.length;
    }

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

ContainerIterator クラスでは、Iterator インタフェースを実装します。

フィールドに、現在の要素の位置を表す index 変数を持たせます。
初期値は 0 (先頭の要素) です。

next メソッドが呼び出されると、現在の index の要素を返します。
そして index を +1 します。

hasNext メソッドでは、index が Container の範囲内かどうかを判定します。

public class ContainerIterator implements Iterator {
                               ^^^^^^^^^^^^^^^^^^^ <---- 実装

    private final Container container;
    private int index;

    public ContainerIterator(Container container) {
        this.container = container;
    }

    @Override
    public String next() {
        final var element = container.get(index);
        index++;
        return element;
    }

    @Override
    public boolean hasNext() {
        return index < container.size();
    }
}

実際に、Iterator で要素にアクセスしてみましょう。

final var container = new Container();

final var it = container.iterator();
while (it.hasNext()) {
    final var element = it.next();
    System.out.println("element = " + element);
}

// 結果
// ↓
//element = aaa
//element = bbb
//element = ccc

無事に、すべての要素にアクセスできました。

これが、GoF によるデザインパターンでの1つである Iterator パターンです。


なぜ反復子(Iterator) が必要なのか?

Iterator パターンを初めて見たかたは、少しまどろっこしいな…と感じるかもしれません。
要素にアクセスするだけなのに、反復子をわざわざ使って、要素へのアクセスも hasNextnext メソッドを使って、と…

例えば、すべての要素にアクセスする 共通のインタフェース を作るだけであれば、次のようなインタフェースでよいかもしれません。

クラス図

public interface Arrayable {
    String[] toArray();
}

Arrayable インタフェースは、toArray メソッドですべての要素を配列へと変換します。

もし 要素が少ない コンテナしか扱わないのであれば、これでも問題ありません。
Iterator パターンよりシンプルで良いですね。

しかし、要素が多くなるコンテナでは問題が発生します。
具体的には、Iterator パターンに比べてメモリ使用量が増え、処理時間も増えます。

コード例で見てみましょう。

次の Container クラスは、

  • get : 要素を取得
  • size : 要素の数を取得

というメソッドを持ちます。

要素は配列で管理しているかもしれないし、別の方法で管理しているかもしれません。
あえてそこは省略しています。

public class Container implements Arrayable {

    ... 省略 ...

    // 要素の取得
    public String get(int index) {
        ...
    }

    // 要素の数を取得
    public int size() {
        ...
    }

    @Override
    public String[] toArray() {
        final var array = new String[size()];
        for (int i = 0; i < size(); i++) {
            array[i] = get(i);
        }

        return array;
    }
}

toArray メソッドでは、要素を配列へと変換します。
そのために、まずは new 演算子で配列を生成します。

final var array = new String[size()];

サイズが 100 くらいなら問題ないかもしれません。
しかし、さらに大きくなるとメモリ使用量も馬鹿にできなくなるでしょう。

次に、要素を配列にコピーする処理です。

for (int i = 0; i < size(); i++) {
    array[i] = get(i);
}

これも、サイズが小さければよいですが、大きければそれだけ処理時間がかかります。

一方、反復子(Iterator) を使えば、余計なメモリ確保やコピー処理は発生しません。
なぜなら、反復子では next メソッドが呼ばれるたびに、要素は Container から直接取得するからです。

public class ContainerIterator implements Iterator {
    private final Container container;
    private int index;

    public ContainerIterator(Container container) {
        this.container = container;
    }

    @Override
    public String next() {
        final var element = container.get(index);
        index++;
        return element;
    }

    @Override
    public boolean hasNext() {
        return index < container.size();
    }
}

このように、反復子を使えば、パフォーマンスの悪化をできるだけ小さくすることができるわけですね。


Java の標準API を利用する例

Java で Iterator パターンを作りたい場合、1から作る必要はありません。
標準API には、Iterator パターン用のインタフェースがすでに用意されているからです。

さらに、標準API を利用すれば、拡張for文 (for-eachループ文) にも自動的に対応できます。

それでは、見ていきましょう。
標準API で用意されているインタフェースは2つあります。

Iterable

このインタフェースを実装すると、オブジェクトが拡張for文("for-eachループ"文とも呼ばれる)のターゲットになることができます。

1つは、Iterable インタフェースです。
Iterator パターンのクラス図でいうと、Aggregate に相当します。

クラス図

各メソッドの使用例については、下記の記事もご参照ください。

Iterator

コレクションのイテレータです。

もう1つは、Iterator インタフェースです。
Iterator パターンのクラス図でいうと、同じ名前の Iterator に相当します。

クラス図

各メソッドの使用例については、下記の記事もご参照ください。

実装例

クラス図

Container と ContainerIterator は、先述した コード例 とほぼ同じです。
違いは、implements する Iterable と Iterator がジェネリクス(総称型) であることです。

public class Container implements Iterable<String> {

    private final String[] elements = {"aaa", "bbb", "ccc"};

    public String get(int index) {
        return elements[index];
    }

    public int size() {
        return elements.length;
    }

    @Override
    public Iterator<String> iterator() {
        return new ContainerIterator(this);
    }
}
public class ContainerIterator implements Iterator<String> {
    private final Container container;
    private int index;

    public ContainerIterator(Container container) {
        this.container = container;
    }

    @Override
    public String next() {
        final var element = container.get(index);
        index++;
        return element;
    }

    @Override
    public boolean hasNext() {
        return index < container.size();
    }
}

使い方も同じですね。

final var container = new Container();

final var it = container.iterator();
while (it.hasNext()) {
    final var element = it.next();
    System.out.println("element = " + element);
}

// 結果
// ↓
//element = aaa
//element = bbb
//element = ccc

また、標準API の Iterable を使うと、要素のアクセスに 拡張for文 (for-eachループ文) が使えるようになります。
Iterator を使うよりも分かりやすくておすすめです。

final var container = new Container();

for (final var element : container) {
    System.out.println("element = " + element);
}

// 結果
// ↓
//element = aaa
//element = bbb
//element = ccc

まとめ

Iterator パターンを使うと、共通のインタフェースでコンテナの各要素に順々にアクセスできます。

もし、Java を使っているのであれば、標準API で用意されている

を利用しましょう。

拡張for文 (for-eachループ文) にも対応できるのでおすすめです。


関連記事

ページの先頭へ