Java : Iterator パターン (図解/デザインパターン)
Iterator パターンとは、GoF によって定義されたデザインパターンの1つです。
コンテナの要素を、共通のインタフェースで順々にアクセスします。
本記事では、Iterator パターンについてコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
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 パターンを初めて見たかたは、少しまどろっこしいな…と感じるかもしれません。
要素にアクセスするだけなのに、反復子をわざわざ使って、要素へのアクセスも hasNext と next メソッドを使って、と…
例えば、すべての要素にアクセスする 共通のインタフェース を作るだけであれば、次のようなインタフェースでよいかもしれません。
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
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ループ文) にも対応できるのでおすすめです。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン