前置き
デザインパターンはクラスの再利用化を促進するものです。 再利用化を促進するとは、 クラスを部品として使えるようにすることであり、1つの部品を修正しても、他の部品の修正が少なくてすむ、ことなのです。
本記事は当ブログ管理人よしたろうによる『GoFデザインパターン入門シリーズ』です。本記事を見るだけで、各デザインパターンの総合的理解ができることをコンセプトにしております。ピンポイントな情報ではなく、理解に必要な周辺知識をできる限り記述していきます。一記事あたりの文章量は多くなる傾向になります。あまりそういった記事が見当たらないこと、自身の学習のためにそういった形式にしております。以前の記事で詳しく紹介した部分はリンクの引用をする場合もあります。
デザインパターンって何?という方は過去記事「Java言語で学ぶデザインパターン入門」シリーズ 第0回 【デザインパターンとは?】をご覧下さい。
本記事では、for文と拡張for文の違いからIteratorパターンを理解していきたいと思います。
ざっくり言えば、拡張for文は通常のfor文で行っていることを簡略化して書けるようになっただけです。つまり、for文でやるような処理をJavaが裏で勝手にやってくれているだけです。これを糖衣構文(sugar syntax)と言ったりします。
ただし、for文と拡張for文では集約オブジェクト(後述)に対するアプローチが全く異なります。その結果として、それぞれのアプローチでの問題があります。
この記事では、まずIteratorパターンで行われている処理が具体的に何をしているのか見ていきます。その後、for文では何がいけないのか?どんな問題が生まれたのか?についてフォーカスし、その問題への一つのソリューションとしてのIteratorパターン、といった形で紹介します。
実は拡張for文の内部実装にはIteratorパターンが適用されています。なので暗黙的に拡張for文がIteratorとなります。
よって、for文の違いと拡張for文の違いが分かれば、自然とIteratorパターンについての理解も進みます。
もしかすると現時点で思った方もいるかもしれませんが、
「拡張for文が暗黙的に Iterator なら、Iteratorパターン知らなくても良くない?」
はい、そうなりますよね?私も最初そう思いました。
良くないんですよ
というわけで、拡張for文だけ理解しておけばいいわけじゃない理由も最後に説明できればと思います。
そんな感じでこの記事は話を進めて行きます。
また、本記事において『集約オブジェクト』とは
- 配列
- コレクション
をとしてます。他にもあると思いますが、本記事では上記を想定しております。また、上記の通りコレクションと配列を区別します。 この記事の文脈で気にしているのは、配列は不変(immutable)・コレクションは可変(mutable)な点です。immutable な Listとかもありますが、そこは応用なのでここでは言及しません。
コレクションとはオブジェクトの集まりを表現するデータ構造、抽象データ型またはクラスの総称で、配列は含まれません。配列は作成された時点で要素数が固定され、Listなどのコレクションは可変であり、要素数は未確定です。また、コレクションとしては以下を定義しています。
- リスト(List)
- セット(Set)
- マップ(Map)
定義ガバガバかもですが、イメージだと思ってもらえれば。
- 前置き
- 1. Iterator パターンのGoF本におけるカテゴライズ
- 2. Iteratorパターンの概要
- 3. for文と拡張for文の違い
- 4. 『拡張for文だけではいけない』理由とまとめ
- 5. おまけ:拡張for文はなにを拡張(継承)しているのか?
1. Iterator パターンのGoF本におけるカテゴライズ
オブジェクトの生成に関するパターン | |
---|---|
Iterator(1つ1つ数え上げる) | 集約オブジェクトの種類・実装・構造に依存せす、統一的に走査し要素を一個一個取り出す方法を提供する。 |
目的 | ||||
---|---|---|---|---|
生成 | 構造 | 振る舞い | ||
範囲 | クラス | Factory Method | Adapter | Interpreter / Template |
オブジェクト | Abstract Factory / Builder / Prototype / Singleton | Adapter / Bridge / Composite / Decorator / Façade / Flyweight / Proxy | Chain of Responsibility / Command / Iterator / Mediator / Memento / Observer / State / Strategy / Visitor |
1-2. 2009年に再定義されたGoFデザインパターンでのカテゴライズ
Core(重要) | Creational(生成) | Periphenal(あまり使用されない) | Other(その他) |
---|---|---|---|
Composite / Strategy / State / Command / Iterator / Proxy / Template Method / Facade | Factory / Prototype / Builder / Dependency Injection | Abstract Factory / Visitor / Decorator / Mediator / Type Object / Null Object / Extension Object/Interface | Flyweight / Interpreter |
詳細は以下の過去記事に記述してますので気になればご覧ください
2. Iteratorパターンの概要
lteratorパターンとは、何かがたくさん集まっているときに、それを順番に指し示していき、全体をスキャンして処理を繰り返すものです。iterateという英単語は何かを『繰り返す』という意味です。iteratorは日本語では反復子と呼ばれることがあります。
集約オブジェクト(配列、ArrayListなど)に対して、各要素に順番にアクセスする方法を提供するためのパターンです。
Listの様な集約オブジェクトと走査方法を与えるクラスを独立させることによって、Listなどの内部仕様に依存しないを提供することを目的としています。
集約オブジェクトとして利用するクラスに走査方法を与えるメソッドが存在する場合。集約オブジェクトとして利用するクラスに変更の必要が生じた際、 その変更が多くの部分に影響を与えるようなことが考えられます。
ですが、単一責任原則に則り集約オブジェクトと走査方法を与えるクラスを分離させた場合。上記の様な場合でも変更すべき点を少なく抑える事ができます。また、必要な走査方法を与えるクラスをユーザが自分で作成することも可能になり再利用性が高まるといった設計になっています。
※ 単一責任原則:『変更する理由が同じものは集める、変更する理由が違うものは分ける。』
この項では、Iteratorパターンの動きについて見ていきたいと思います。
2-1. サンプルプログラムについて
ここで作るプログラムは、 本棚 (BookShelf) の中に本 (Book) を入れ、その本の名前を順番に表示するというものです。
2-1-1. サンプルプログラムのクラス図
2-1-2. サンプルプログラムのコード
引用元:* Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
(コメントは私がつけてます。見やすい様にメソッドの記述位置もいじってます。)
2-2. Iteratorパターンの登場人物と対応コード
名前 | 解説 |
---|---|
Iterable | 集合体を表すインタフェース |
Iterator | 集合体に対しての繰り返し処理を表すインタフェース |
Book | 本クラス |
BookShelf | 本棚クラス |
BookShelfIterator | 本棚に入っている本の名前を順番に一つずつ取得するクラス |
Main | 動作確認用クラス |
以下にコードを記述(一部簡略化)
2-2-1. ~集合体~(Itarableインターフェイス)
Iterable.class
/** * 集合体を表すインターフェイス * * / public interface Iterable<T> { // 集約オブジェクトを一つずつ数えあげる Iterator<T> iterator(); }
- Iterableインターフェイスは 集約オブジェクトを表現する
2-2-2. ~反復子~(Iterator インターフェイス)
Iterator.class
/** * 数え上げて走査を行うインターフェイス * */ public interface Iterator { // 次の要素が存在するかどうか判定 boolean hasNext(); // 現在の値を返して、次の位置に進める E next(); }
- Iteratorインターフェイスは繰り返し処理を表現する
- BookShelfIteratorクラスで実装されているインターフェース
- 集約オブジェクトの要素を順番に一つずつ取得するための抽象メソッド、next(), hasNext() が定義されている
- 呼び出されたら現在の要素を返却し、次回の呼び出し時に次の要素を返すように内部状態を次に進める働きもある
- returnCurrentElementAndAdvanceToNextPosition と呼ぶべき動きをする
- 詳細はBookShelfIteratorクラスに
2-2-3. ~動作確認~( Mainクラス)
import java.util.Iterator; /** * 動作確認用クラス * */ public class Main { public static void main(String[] args) { // 本棚として、BookShelfインスタンス生成 BookShelf bookShelf = new BookShelf(2); // 本棚に入れる本として、Bookインスタンス生成して棚に入れる bookShelf.appendBook(new Book("転生したらスライムだった件")); bookShelf.appendBook(new Book("月が導く異世界道中")); bookShelf.appendBook(new Book("Reゼロからはじまる異世界生活")); bookShelf.appendBook(new Book("野人転性")); // 明示的にIteratorを使う方法 → Iterator 生成 Iterator<Book> it = bookShelf.iterator(); // 次の本がある限り、本を数え上げる while (it.hasNext()) { Book book = it.next(); System.out.println(book.getName()); } System.out.println(); // 拡張for文を使う方法、やっていることは上記と同じ for (Book book: bookShelf) { System.out.println(book.getName()); } System.out.println(); } }
2-2-4. ~具体的な集約オブジェクト~(BookShelfクラス、及びBookクラス)
Book.class
/** * 本を表すクラス */ public class Book { // コンストラクタ内で初期値を設定する為のフィールドの宣言 private String name; // コンストラクタ public Book(String name) { this.name = name; } public String getName() { return name; } }
BookShelf.class
import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * 集約オブジェクト(配列やコレクション)を表現するクラス。ここでは本棚を表現 * Iterable = 反復可能・繰り返す事ができる という意味の英単語 */ public class BookShelf implements Iterable<Book> { private List<Book> books; // コンストラクタ public BookShelf(int initialsize) { this.books = new ArrayList<>(initialsize); } // main()のbookShelf.appendBook()で呼び出される // 本を追加する public void appendBook(Book book) { books.add(book); } // BookShelfIterator の hasNext()で呼び出される // 本棚の冊数を取得 public int getLength() { return books.size(); } // BookShelfIterator の next()で呼び出される // 指定されたindex番号の本を取得 public Book getBookAt(int index) { return books.get(index); } // Iterable インターフェイスで定義されている iterator()を実装 // 本棚(BookShelfクラス)を走査するイテレーターを作成 @Override public Iterator<Book> iterator() { return new BookShelfIterator(this); } }
2-2-5. ~実装された反復子~ (BookShelfIteratorクラス)
BookShelfIterator.class
import java.util.Iterator; import java.util.NoSuchElementException; /** * 実際に繰り返し処理を行うクラス、Iterator = 反復子という意味の英単語 * 集約オブジェクト(bookShelf)に持たせず分離させているのが重要 */ public class BookShelfIterator implements Iterator<Book> { private BookShelf bookShelf; private int index; // コンストラクタ public BookShelfIterator(BookShelf bookShelf) { this.bookShelf = bookShelf; this.index = 0; } // main()で呼び出し next()を使用しても問題ないか調べる // 次の本が存在するか判定 @Override public boolean hasNext() { // bookShelf.getLength()の値はBookShelfインスタンス生成時点で固定数 // next()にて集合体の要素を取り出し後 index はインクリメントされる // 最後の本を取得した後はfalseになる if (index < bookShelf.getLength()) { return true; } else { return false; } } // 現在の要素を返しつつ、indexを次の位置に進める。正しく命名するなら、 // returnCurrentElementAndAdvanceToNextPosition @Override public Book next() { if (!hasNext()) { throw new NoSuchElementException(); } // indexで集約オブジェクト内の要素を指定 Book book = bookShelf.getBookAt(index); // for文の i++ 相当 // 集約オブジェクトを順番に全体走査・終端判定に必要 index++; return book; } }
2-3. 出力
- Iteratorを明示的に使用した出力
転生したらスライムだった件 月が導く異世界道中 Reゼロからはじまる異世界生活 野人転性
- 拡張for文を使用した出力
転生したらスライムだった件 月が導く異世界道中 Reゼロからはじまる異世界生活 野人転性
拡張for文の裏側には Iterator パターンが使用されています。 動作も、出力も同じです。
ここまでで、Iteratorパターンの動きがなんとなくご理解いただけたでしょうか? ここだけ見ても、全部を理解するのはなかなか出来ないかと思いますが.....(私は無理です)
重要なのは
- 集約オブジェクト(本棚)と走査方法(イテレータ)のクラスが分離されている事(わざと一緒にされてないのです)
- 上記の詳細になるかもしれませんが、いま集約オブジェクトをどこまで辿ったか?を覚える仕組みも集約オブジェクトから分離されています(i++ です)
- 本棚にある本の名前を取得したい側(今回はMain.class)は、実装の詳細を知らなくても集合オブジェクトにある Iterator() を呼び出すだけで取得できる事(インターフェイスの使用)
- 拡張for文を使用するときに、裏では主にhasNextメソッド / nextメソッド が動いているがそれらの実装が隠蔽されている事 (拡張for文の使用時にそんなこと意識しませんよね?)
3. for文と拡張for文の違い
前置きでも説明しましたが、拡張for文は暗黙的にIteratorですので、for文と拡張for文の違いが分かれば、Iteratorパターンの理解が進みます。
3-1. for文の特徴
for (i = 0; i < array.length; i++) { System.out.printIn(array[i]); }
- index番号指定・ループカウンタ用の変数の初期化(最初に一回)
- 終端判定
- カウンタ変数をインクリメント
「終端判定」はいわゆる
i < data.length;
の部分だけど、これは集約オブジェクトのサイズを知らないと記述できない。言い換えれば集約オブジェクトの構造を知る必要があり、これは場合によって大きな問題を孕んでいます。後述します。
「カウンタ変数をインクリメント」 は
i++
の部分ですが、これは集約オブジェクトの要素全体を順々に走査していく働きを担っています。
また、以下の様に i の初期値を変更したり、アクセスする要素のインデックス番号を指定することで、任意の要素にアクセスする事ができます。これをランダムアクセスと言います。以下は解り易く書いただけで、for文 自体がランダムアクセスです。
for (i = 5; i < array.length; i++) { // i が偶数の場合 if(i % 2 = 0) { System.out.printIn(array[i]); } }
ランダムアクセスを可能にするには、サイズを返すメソッドや指定位置のメソッドが必要です。それぞれ、終端判定とインクリメント・if 文 などが該当しますかね。 前述した通り、ランダムアクセスするためには集約オブジェクトの構造を理解する必要があります。これは前述しましたが、大きな問題があります。
3-1-1. for文の問題点
- ランダムアクセスのため、アクセス対象集約オブジェクトのデータ構造を把握する必要があり、把握した上でサイズを返すメソッドや指定位置のメソッドが必要になる。
- ex 集約オブジェクトが配列だった場合
- 配列の名前
- 先頭・末尾のインデックス番号
- 配列のサイズ など
- どの様なデータ構造の取り出し処理をするのか分からない場合、すべての場合を想定してループを用意する必要がある
- 非現実的ですよね。実際はデータ構造に合わせて処理を書き直すと思いますが、その処理は局所最適・再利用不可なコードでぱっと見なにしてるか解りません。
- 取り出した後のデータの処理も取り出し方に依存する。
- 局所最適・再利用不可なコードでぱっと見なにしてるか解りません。
- for 文を持つクラスと集約オブジェクトが密結合になる
- 集約オブジェクトの構造が変更された場合、取り出し方の変更が必要となりそれに伴い、取り出した後の処理も変更が必要になる。
- 要素を取り出した後に、その要素を処理するコードの再利用・共有が出来ない
- 取り出し方が、集約オブジェクトの種類・内部構造に依存するからですね。
- もし存在しないインデックス番号に間違えてアクセスしようとしたら、Exception
- ArrayIndexOutOfBoundsException(配列の場合)
- IndexOutOfBoundsException(コレクションの場合)
- for文が正しく動くかどうかは、実際に動かさないと分からない。
書いてみたら思ったよりもありました。6・7はほぼ同じことですが。
逆に良い点としては以下になります
引用元:これだけ押さえよう! Javaのforeach関連構文・機能の紹介
インデックスを変数で表現すると、例えば要素へのアクセスを1個飛ばしにできますし、先読みもできるので、ループ処理を柔軟に作れます。ですから、for文/while文を使うことはこれからもなくならないでしょう。
異論・反論、ございましたら是非コメントにてご意見いただければ嬉しいです!!
3-2. 拡張for文の特徴
では、上記のfor文の問題点を拡張for文がどう解決したのか。 拡張for文の動きから順に見ていきましょう。
for (int e : array) { System.out.printIn(e); }
foreach文は裏側で以下の様なことをしています。
- 取得する要素があれば true
- あったら取得するためのメソッドを呼び出す
- 呼び出したメソッドで、まだ取得していない次の要素を取得する
- 取得した後に、要素の指定位置を次に進める
- 最後の要素を取得したら false
- 繰り返し処理の終了
実装の詳細が隠蔽されているので普段気にすることはありませんが、拡張for文の内部実装のメソッドは Iterator インターフェースで定義されている以下の抽象メソッドです。
終端判定・カウンタ変数のインクリメント・要素返却 がこの実装で行われいて、実装者からすると隠蔽されています。拡張for文を使うときにこの二つのメソッドを意識して使うことはないですよね?うまいこと隠蔽してくれてます。
集約オブジェクトがなんであれ、 次があるか判定しあれば次の要素を取得する。この集約オブジェクトがなんであれ、というのが重要です。
配列・ArrayList・Map・Set、内部構造が多次元かそうでないか?そういったことも関係なく、一個一個要素を取り出すということをしてくれます。
これをシーケンシャル(順次)アクセスと言います。for文はランダムアクセスでした。ランダムアクセスは、アクセス対象のデータ構造を把握する必要がありました。 これに対してシーケンシャルアクセスは名前の通り、集約オブジェクトに対して順番にアクセスします。なので、現在どの要素にポインタを合わせているかの情報を保持する必要がありますが、for文と同じでカウンタ変数をインクリメントしていくだけです。
ではここで、for文の問題点を改めて確認します。
- ランダムアクセスのため、アクセス対象集約オブジェクトのデータ構造を把握する必要があり、把握した上でサイズを返すメソッドや指定位置のメソッドが必要になる。
- どの様なデータ構造の取り出し処理をするのか分からない場合、すべての場合を想定してループを用意する必要がある
- 取り出した後のデータの処理も取り出し方に依存する
- for 文を持つクラスと集約オブジェクトが密結合になる
- 要素を取り出した後に、その要素を処理するコードの再利用・共有が出来ない
- もし存在しないインデックス番号に間違えてアクセスしたら、Exception
- for文が正しく動くかどうかは、実際に動かさないと分からない。
一個一個、確認していきましょう。
3-2-1. for文の問題点に対する拡張for文のソリューション
- ランダムアクセスのため、アクセス対象集約オブジェクトのデータ構造を把握する必要があり、把握した上でサイズを返すメソッドや指定位置のメソッドが必要になる。
→ シーケンシャルアクセスになるためデータ構造の把握は不要
問題 2・3 ・4・5・6・7は、問題1に依存してましたので一挙に消え去りました。
2. どの様なデータ構造の取り出し処理をするのか分からない場合、すべての場合を想定してループを用意する必要がある
3. 取り出した後のデータの処理も取り出し方に依存する
問題2・3に関しては、取り出し方とその後の処理は密接に関係しています。取り出し方が『順に一個ずつ取り出される』に統一されたのであれば同様に取り出した後の処理も統一的に扱える様になります。取り出し方を気にする必要がなくなりますね。
4. for 文を持つクラスと集約オブジェクトが密結合になる
問題4に関しても問題2・3の結果、密結合になるという話なので問題4は自動的に解決します。集約オブジェクトの種類・構造に関係なく、要素を一個一個 順番に取り出すことができる様になったからです。もし、途中で集約オブジェクトのデータ構造が変更されても取り出し方の変更は無くなるか小さくなるので、取り出し後の処理の影響も抑える事ができます(疎結合の実現)。
5. 要素を取り出した後に、その要素を処理するコードの再利用・共有が出来ない
問題5も問題2・3の結果、局所最適化されてしまい再利用性が損なわれるという話でした。 統一的な取り出し方法が提供されるのであれば、取り出した後の処理も統一的に扱える様になるので、取り出した後の処理を再利用する事ができる様になります。
6. もし存在しないインデックス番号に間違えてアクセスしたら、Exception
7. for文が正しく動くかどうかは、実際に動かさないと分からない。
問題6・7についても、終端を意識する必要がないので消えます。
- 『要素が存在するかしないか判定し、あれば取得する』
ただこれだけをしているのです。
for文も拡張for文もやりたいことは大雑把に言えば、『集約オブジェクトから要素を取り出すこと』です。
拡張for文は、集約オブジェクトの内部構造を隠した(意識させない)まま、それぞれの要素にアクセスが可能になります。その結果、異なる内部構造を持つリストの要素に同じインターフェースでアクセスできます。
インターフェースに書いてあるのは「What:何をするか」ですね。実際に「How:どうやって?」実現するのかは implemants(実装)したクラスが責務を負い、そこにプログラミングをします。
本記事では、Iterator インターフェースのことです。
Iterator インターフェースには、インターフェイスに課せられた振る舞いを実現するためのメソッドが定義されています。 課せられた振る舞いとは「集約オブジェクトがなんであれ、要素をひとつずつ取り出す」事でした。
それを実現するために使用したのは以下の二つのメソッドです。
(サンプルプログラムの BookShelfIteratorクラスの@overrideしてる二つのメソッドのこと)
多次元配列などの場合、for文であれば集約オブジェクトの内部構造を考慮したコード、配列の構造 にタッチした実装が必要になります。for文のネストしたりですね。それに対して、拡張for文の場合は順番に取り出すという事だけなのでネストしたとしても、記述は単純になります。
具体的にはfor文で必要だった以下の様なインデックス管理など、目に見える部分・見えない部分の作業の両方が不要になります。
- index番号指定・ループカウンタ用の変数の初期化(最初に一回)
- 終端判定
- カウンタ変数をインクリメント
- 上記を記述するために必要な集約オブジェクトのデータ構造の把握
集約オブジェクトの種類・内部構造がなんであれ、順番に一個一個要素を取り出すという処理だけを行います。
そして呼び出し側からすると、インターフェイスの実装クラス詳細は関係もないし興味もない部分になります。呼び出す側にとって最も関心があることはインターフェイスに期待する役割が果たされることです。その役割をどのように果たすのかはどうでもいいことです。
あれですね、異世界転生で王様がどうやったらいいか知らんけど、勇者たちよ魔王討伐してこいや。やり方は任すわ!みたいなんと一緒ですね。
- 呼び出し側
- 王様・呼びつけるだけ呼びつけてほったらかしにする人。呼んだら自動的にどうにかなると思い込んでる。
- インターフェイス
- 異世界人召喚術(陣)
- 引数
- チート能力
- 呼び出される側・実装クラス
- 勇者・実際に頑張る人。一番苦労する人。仲間失ったりするかもしれないし、血反吐はく様な修行したり、いきなり呼び出されて家族と二度と会えなくなったりする。その苦労は王様には届かない。現実の私達でもある。
引数のチート能力は神とかが渡してるから、神クラスみたいなのがあってそれに依存してるんですね。王様知らないとしたらうまいこと隠蔽されているのでしょう、すごい!
上記を拡張for文で置き換えると
- 呼び出し側
- 開発者・私達もしくは他の誰か。呼び出せば勝手に要素が一個ずつ順番に最後まで取れると思い込んでいる。
- インターフェイス
- 引数
- なし
- 呼び出される側・実装クラス
- サンプルコードのBookShelfIteratorクラス、hasNext() / next() の実装が記述される。一番苦労する箇所。保守性・可読性・拡張性などに配慮し、変更に強い実装にしなくてならない。その苦労は私達には届いたり届かなかったりする。現実の私達である。
4. 『拡張for文だけではいけない』理由とまとめ
4-1. 「拡張for文が暗黙的に Iterator なら、Iteratorパターン知らなくても良くない?」にたいする答え
もし、言語ごとに提供されているデフォルトのIteratorメソッドの実装で対応できないデータの抽出方法を使用したい場合どうするんですか?という問いが答えです。それに適したIteratorメソッドの実装を行う必要があります。 「Iteratorパターンを知らないで良い」ということは言語の提供する範囲でしかプログラミングができなくても良い、といっているのと同義なのです。
集約オブジェクトの探索の違いとして『深さ優先』『幅優先』探索というものがあります。
引用元:うさぎでもわかる離散数学(グラフ理論) 第12羽 幅優先探索・深さ優先探索 | 工業大学生ももやまのうさぎ塾
これは単純にアルゴリズムの違いです。どちらにしろ、一つずつ要素を取り出すという点に着目して考えれば、Iterator インターフェイスとして行いたい振る舞いは同じなので Iterator インターフェイスの適用を考える事ができます。
アルゴリズムを実現する実装は必要ですが、その実装を他のクラスに抽出したり、インターフェイスの実装クラスに抽出して、そのインスタンスを渡される様にすることでポリモーフィズムが実現できます。
また、Iteratorインターフェイスの実装クラスのアルゴリズムを Strategy パターンで切り替えれば、どちらの探索でもインターフェイスは同じままにする事も可能です。この様なポリモーフィズムによって、インターフェイスを対象にした処理を統一する事ができ、その処理の再利用性が高めるとともに実装クラスの隠蔽が可能になります。
ポリモーフィズムとはあるオブジェクトへの操作が、呼び出し側ではなく受け手のオブジェクトによって定まる特性です。同じ名前のメソッドを呼び出した時に、実行される処理がインスタンスの種類によって変わるという機能を意味しています。
Strategy パターンはアルゴリズム実装のための専用オブジェクトを複数作っておき,その中から使うものだけを動的に選んで実行するものです。Strategy パターンでアルゴリズムを切り替える具体的な方法として以下から引用しつつ、一部追加してます。
Strategyパターンでは、アルゴリズムの部分をほかの部分と意識的に分離します。そしてアルゴリズムとのインターフェースの部分だけを規定し、委譲によってアルゴリズムを利用します。 これは、プログラムを複雑にしているように見えますが、そうではありません。例えば、アルゴリズムを改良してもっと高速にしたい場合、Strategyパターンを使っていれば、Strategy役のインターフェースを変更しないようにして、アルゴリズムをだけを追加、修正すればいいのです。委譲というゆるやかな結びつきを使っているのため、アルゴリズムを用意に切り替えることができます。 また、ゲームプログラム等では、ユーザーの選択に合わせて難易度を切り替えたりすることにも使えます。
4-2. for文・拡張for文・ついでにIteratorメソッドの違い[表]
for文・拡張for文・Iteratorメソッドの違い(赤文字はデメリット) | ||||
---|---|---|---|---|
for文 | 拡張for文 | Iteratorメソッド | 理想 | |
ループ構文 | 必要 | 必要 | 必要Iterator生成 + for or while | 不要 |
集合体へのアクセス方法 | ランダム | シーケンシャル | シーケンシャル | 両方可能 |
ループ処理の柔軟性 | 高い | 低い | 高い | 高い |
ループ中に要素削除 | 簡単 | 面倒 | 簡単 | 簡単 |
ループ中の取り出し処理 | 必要 | 不要 | 必要 | 不要 |
集合体の構造把握 | 必要 | 不要 | 不要 | 不要 |
インデックス管理 | 必要 | 不要 | 不要 | 不要 |
結合度 | 高い | 低い | 低い | 低い |
再利用性 | 低い | 高い | 高い | 高い |
動作確認 | 実行必要 | コンパイルエラー | 実行必要 | コンパイルエラー |
for文の使い所は、上記でも引用しているのですが以下になります。
引用元:これだけ押さえよう! Javaのforeach関連構文・機能の紹介
インデックスを変数で表現すると、例えば要素へのアクセスを1個飛ばしにできますし、先読みもできるので、ループ処理を柔軟に作れます。ですから、for文/while文を使うことはこれからもなくならないでしょう。
また、拡張for文にも問題というかデメリット・できない事・特長的な性質があります。
- 必ず配列・コレクションの先頭からの処理となる
- 後ろから処理をしたい場合はあらかじめ逆順にソートし直すなどの処置が必要
- 一つ飛ばしなどにしたい場合も工夫が必要
- 取り出した要素を削除する場合は簡単にはできない
それぞれの詳細は省きます。 いずれ Stream API の事なども記事にしたいのでその際にでも書こうと思います。
参考記事:これだけ押さえよう! Javaのforeach関連構文・機能の紹介
ループの制御という面では拡張for文よりは柔軟に書ける点でIteratorメソッドの出番があります。 ただ、IteratorはIteratorでもちろんデメリットがあります。for文と同じになります。
- 自分で要素を取り出さなければならない
- 実行してみなければ正しく動くか分からない
基本トレードオフなのでそれぞれの特性を知り、適材適所で使い分ける考え方が必要です。
※ Iteratorメソッドを明示的に使用する方法
// while文を使う場合 List<Integer> list = Arrays.asList(1, 2, 3); // ListはIterableを継承しているインターフェイス Iterator<Integer> it = list.iterator(); // ListからIteratorを取得する // Iteratorに「次」の要素があるか確認し、 while (it.hasNext()) { // Iteratorから取り出す Integer e = it.next(); System.out.println(e); }
// for文を使う場合 List<Integer> list = Arrays.asList(1, 2, 3); // Iteratorの取り出しと、hasNextを1行で書く for (Iterator<Integer> it = list.iterator(); it.hasNext();) { Integer e = it.next(); System.out.println(e); }
ちなみにループ構文が不要なのは以下になります。
Stream.forEach(Consumer)
構文
// Stream.forEach()のメソッドシグネチャ、いずれもConsumerを引数に取る void forEach(Consumer<? super T> action); void forEachOrdered(Consumer<? super T> action);
例
List<String> list = Arrays.asList("転スラ", "月導", "Reゼロ", "野人", "いせおじ"); // (ラムダ式でConsumerのインスタンスを生成) list.stream().forEach(e -> System.out.println(e)); // (メソッド参照でConsumerのインスタンスを生成) list.stream().forEach(System.out::println); // 転スラ, 月導, Reゼロ, 野人, いせおじ, が出力される
Iterator.forEachRemaining(Consumer)
Streamによる中間処理が不要なのであればこれ
List<String> list2 = Arrays.asList("転スラ", "月導", "Reゼロ", "野人", "いせおじ"); // ListはIterableを継承しているインターフェイス list2.forEach(e -> System.out.println(e));
4-3. Iteratorパターンについてのまとめ
本質は集約オブジェクトの種類・構造に関係なく、要素を一個ずつ 順番に取り出すことができる
「終端の見えない構造でも、大きさもよく解らない構造でも、それに対して一個ずつ要素を取り出すことができれば、一個ずつ取り出すことが終わった後に使うコードは、再利用または共有できる」
- 単一責任原則に則り、集約オブジェクトと走査方法のクラスが分離されている(疎結合)
- 集約オブジェクトの変更があっても走査方法の変更は低く抑えられる。
- いま集約オブジェクトをどこまで辿ったか?を覚える仕組みも集約オブジェクトから分離されている。
- インターフェイス(ex. Iterator)の使用で振る舞いの抽象化・実装の隠蔽・ポリモーフィズムが実現できる
- 振る舞いの抽象化(インターフェイスを呼び出すだけで目的を達成させる)
- 実装の詳細の隠蔽化
- 結合度が高くて変更に弱いメソッドの実装詳細と、それを呼び出す側の間にインターフェイスを置くことで呼び出し側から実装の詳細が隠蔽され結合が弱まる。
- サンプルコードで言うと、mainクラスとBookShelfIteratorクラスの間のBookShelfクラスのIteratorメソッドが存在してる部分。
- BookShelfクラスは繰り返し処理される対象としての集合体を表現する Iterable インターフェイスを実装している集約オブジェクト。
- インターフェイスを間に入れることで、上記で言う「集約オブジェクトと走査方法のクラスが分離」が実現される。
- 他のクラスに具体的な実装詳細を分離するのではなく、インターフェイスに振る舞いとそれの実現に必要な抽象メソッドも定義して分離させている。
- これにより、期待する機能の実装の手続きを画一的にする事ができ、かつ実装は隠蔽されているので必要であれば柔軟に影響少なく変更対応ができる様になる。
- 拡張for文を使用するとき、裏では主にhasNextメソッド / nextメソッド が動いてるのですが、意識しませんよね?(実装の隠蔽化・ポリモーフィズム)
- 結合度が高くて変更に弱いメソッドの実装詳細と、それを呼び出す側の間にインターフェイスを置くことで呼び出し側から実装の詳細が隠蔽され結合が弱まる。
- 上記の結果、再利用性が上がり変更にも強くなる
- インターフェイスは手続きを画一化し、呼び出す側と呼び出される側の結合度を下げ、実装も隠蔽できる。実装の詳細が変わっても、インターフェイスを呼び出す側には関係がないので、変更の影響箇所を抑える事ができる。
- 集約オブジェクトの種類・構造に関係なく、要素を一個一個 順番に取り出すことができるのがIteratorパターン。もし、途中で集約オブジェクトのデータ構造が変更されても取り出し方の変更は無くなるか小さくなるので、取り出し後の処理への影響を抑えられる。
- 統一的な取り出し方法が集約オブジェクトの外に存在することにより、それを前提とした取り出した後の処理も統一的に扱える様になる。これにより、取り出した後の処理を再利用・共有が可能になる。結果、似た様な処理のコードの重複・総量は減り保守し易くなる。
iterator パターンとは、配列やそれに類似する集約的データ構造(コレクション・DOMツリー・ディレクトリ / ファイル構造など)の各要素に対する数え上げ・取得の抽象化です。集約オブジェクトに対する数え上げを統一的に扱えるようにし、集約オブジェクトの内部構造を意識させずに一個づつ要素を取り出すという実装を抽象化しています。
大事なのは、パターンやインターフェイスを利用する側のコードの共有・再利用の実現です。そのためには、ポリモーフィズムやインターフェイスの使用による情報隠蔽・振る舞いの抽象化が必要です。また、何を共有・再利用するのかの認識も重要です。
twadaさん曰く
オブジェクト指向の再利用性ってやつは、差分プログラミングの実装の再利用ではなくってポリモーフィズムを使うことによる(デザインパターンを)利用する側のコードの再利用である。という風にうまく説明したんですよね(GoFの)デザインパターンは。それによって、なるほど。上手くプログラミングするってのはこういうことなんだな。で、再利用性を高めるってのは、自分(達)が誤解していて再利用する側はこっち(差分プログラミングの実装)じゃなくてそっち(デザインパターンを利用するコード)だったんだ、みたいな。
このあたりの「GoFデザインパターンの教え」に関しては以下の過去記事、「2. GoFデザインパターンの教え」にて詳細の記述があります。
*引用元:継承 (プログラミング) - Wikipedia *
継承の目的
差分プログラミング(difference coding)とは、クラス間の共通構成を、各クラスの特有構成に引き継がせるようにして、重複構成の削減と、分類体系化をもたらすことを目的にした継承の用法である。これは、クラスに新機能を付け足しての手軽なクラス拡張目的と、クラスの共通部分を括りだして体系化するクラス分類目的の双方に使われた。
差分プログラミングは、継承の元々の用法であり、プログラムの再利用性と保守性を高めると見なされていたが、後年になると階層分散配置されたデータとメソッドの把握のしづらさによる弊害の方が目立つようになって、この用法を否定する傾向が強くなった。同時にその代替としてのコンポジション(合成)(英語版)が重視されるようになっている。
5. おまけ:拡張for文はなにを拡張(継承)しているのか?
拡張for文は英語にすれば、extend ですが何を extend しているのか??
要素がある限りループし要素がなくなればループ終了をするという動作が拡張されたのです。 これ以前は、forループ内でカウンタ変数をインクリメントしながら一つ一つ取り出すという形しか採用されてなかった様です。
これはJava誕生時の時代背景が関係しています。C言語ユーザをJavaユーザに呼び込んでJavaのシェアを拡大するために、上記の様なC言語ユーザにとってなじみ深い書式を採用するしかなかったらしい。マーケティング上の事情のようです。
話を戻しますが、「要素がある限りループし要素がなくなればループ終了するという動作」それは Iterable インターフェイスを実装したクラスで拡張 for 文 が使用できる様になった。という意味です。
仕様書には、
java.lang.Iterable インタフェースを継承しているものが拡張 for 文で使用できる
という記載があります。
public interface Iterable
このインタフェースを実装すると、オブジェクトが拡張for文("for-eachループ"文とも呼ばれる)のターゲットになることができます。
実は拡張for文の構文は厳密には以下になります。
// 拡張for文の構文 for (取り出す要素の変数宣言 : 配列あるいはjava.lang.Iterableのインスタンス) { 繰り返し処理; }
例えば、java.util.Collection インタフェースは次のような記述です。
package java.util; public interface Collection<E> extends Iterable<E> { ...
CollectionインターフェースはIterableインターフェースのサブインターフェースだったのです!!結構、びっくりしました…
余談ですが、java.util.Collectionは,オブジェクトの集合(コンテナ)の持つべき振る舞いを定義するインタフェースです。継承するインタフェースは2つあります。順序付きで要素を格納するjava.util.Listインタフェースと,重複のない集合であるjava.util.Setインタフェースですね。
開発者が実装する際は、サブインターフェースなんか使わないのかなって思います。以下の様に多重実装できますし。どうなんでしょうか?
class クラス名 implements インターフェイス名①, インターフェイス名②, .... { ...... }
ただここで、CollectionインターフェースをIterableインターフェースのサブインターフェースとすることで、その後のCollectionインターフェースの実装クラスのインスタンスに対しては自動的にIterator が実装され、拡張for文が使用可能になるんですね。
Collectionインターフェースのサブインターフェースや実装クラスは下記をご覧ください。
インタフェース java.util.Collection の使用 (Connected Device Configuration (CDC), バージョン 1.1.2)
大雑把な言い方ですが、Collectionインターフェイスを実装すると、Iterableインターフェースを実装することになり、Iterableインターフェースには iteratorメソッドが宣言されているため、こいつをoverrideする必要があります。
そのため、Iteratorインターフェースを実装したクラスが必要となります。 Iteratorインターフェースは集約オブジェクトにたいして、一個ずつ要素を取り出すという用途を実装クラスに実現させるためのメソッドが定義されています。
何回か書いてますが、
ですね。
5-1. Iterable・iteratorインターフェースのソースを見てみましょう。
(上でも記載してますが)細かいコメントや今回登場しないメソッドは省き、一部コメントの日本語訳(DeepLでの翻訳文まま)を記載してます。あと「:」のあとの文章は、独自につけたコメントになります。
またIterator インターフェースは、Iterable インタフェースによって生成されるインターフェースです。
5-1-1. Iterable インターフェイス
集約体を表すインターフェイスです。 Iterableとは「繰り返し処理できるもの」を表現しているインターフェイスともいえます。
Iterable インターフェースは Iterator の対象となる集約オブジェクトのインターフェースに extends(継承)されています(代表的なのは、上記のCollectionインターフェース)。
/** * Implementing this interface allows an object to be the target of the "foreach" statement. * このインタフェースを実装することにより、オブジェクトを拡張されたfor文 * ("for-each ループ" 文と呼ばれることもある)のターゲットにできます。 */ public interface Iterable<T> { /** * Returns an iterator over a set of elements of type T. * * @return an Iterator. */ Iterator<T> iterator(); }
5-1-2. Iteratorインターフェイス
Iterator には集約オブジェクトがなんであれ、 次があるか判定しあれば次の要素を取得するためのメソッドがあります。
public interface Iterator<E> { /** * Returns {@code true} if the iteration has more elements. * (In other words, returns {@code true} if {@link #next} would * return an element rather than throwing an exception.) * * @return {@code true} if the iteration has more elements * :次の要素が存在するか調べる :存在すればTrue :次の要素が存在しない、もしくは最後の要素にたどり着けばFalse :ループの終了条件として使用する :最後の要素を得る前はtrue,最後の要素を得た後はfalseになる */ boolean hasNext(); /** * Returns the next element in the iteration. * * @return the next element in the iteration * @throws NoSuchElementException if the iteration has no more elements * :次の要素を得る :戻り値は「現在の要素」です :現在の値を返して、次の位置に進める */ E next(); ........ }
5-2. 拡張for文、コンパイル後のコードは実際どうなってるのか?(期待通りにならず)
拡張for文を逆コンパイルしたらわかるはずだったけど上手い事できませんでした.....
コンパイル対象ファイル
public class Main { public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { list.add(i); } for (Integer i : list) { System.out.println(i); } } }
public class Main { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); for (int i = 0; i < 10; i++) list.add(Integer.valueOf(i)); for (Integer integer : list) System.out.println(integer); } }
欲しかったデコンパイル結果(例)
For example, this code: List<? extends Integer> l = ... for (float i : l) ... will be translated to: for (Iterator<Integer> #i = l.iterator(); #i.hasNext(); ) { float #i0 = (Integer)#i.next(); ...
引用:Java言語仕様(Java Language Specification)
なんでなんだろう??