Deep Side of Java〜Java 言語再入門 第3回 〜 クラス設計とデザインパターン

やさしいデザインパターン

Iterater






Iterater

イテレータはもう既に StringTokenizer クラスで使い方を見た Enumeration インターフェイスでおなじみである。しかし、デザインパターンとして見た時には、この「イテレータ」が具体的なデータ構造とは切り離されたかたちで順次アクセスを実現することに注目するのである。つまり、具体的な実装が、配列であってもリンクリストであっても、あるいは二分木であっても、実装とは無関係に順次アクセスを実現することは、カプセル化の理念から言って重要なことである。


配列に対するイテレータ

たとえば、配列に対して Enumeration interface を実現してみよう。

public class ArrayIterator implements Enumeration {
     private Object [] array;
     private int counter = 0;

     public ArrayIterator( Object [] o ) { /* 配列に対するイテレータ */
          array = o;
     }

     public boolean hasMoreElements( ) {
          if( array.length >= counter ) {
               return false;
          }
          return true;
     }

     public Object nextElement( ) {
          return array[counter++]; 
     }
}

たとえば、これは次のようにして使える。

     public void sub( String [] args ) {
          int i;
          Enumeration ai = getEnumeration( args );
          for( i = 0; ai.hasMoreElements(); i++ ) {
               System.out.println( i + ":" + ai.nextElement() );
          }
     }
     public Emumeration getEnumeration( Object [] at ) {
          return new ArrayIterator( args );
     }

この時、呼び出し側では具体的なデータ構造が何であるかは無関係に、「列挙」が可能になるのである。もし、この ArrayIterator のコンストラクタとして Vector 型や連結リスト、二分木などを取るようにしてやったとしたら、単純だが汎用的な列挙インターフェイスになることは明白である。


連結リストに対するイテレータ

たとえば、連結リストの場合では次のようになる。

public class LinkedListIterator implements Enumeration {
     private LinkedList ll;
     private int counter = 0;

     public LinkedListIterator( LinkedList o ) {
           ll = o;
     }

     public boolean hasMoreElements( ) {
          if( ll.size() >= counter ) {
               return false;
          }
          return true;
     }

     public Object nextElement( ) {
          return ll.get(counter++); 
     }
}

それこそ、こうするだけでよい。

     public void sub( Object args ) {  
     /* 実際には引数に LinkedList クラスのインスタンスを渡す */
          int i;
          Enumeration ai = getEnumeration( args );
          for( i = 0; ai.hasMoreElements(); i++ ) {
               System.out.println( i + ":" + ai.nextElement() );
          }
     }
     public Emumeration getEnumeration( Object at ) {
          return new LinkedListIterator( (LinkedList)args );
     }

つまり、具体的なロジックに手を入れなくても、操作を配列の場合でも連結リストの場合でも共通化できるのである。

ここでのミソはやはり getEnumeration() を Factory Method として分離してあることでもある。だから、もし具体的な実装データ型が変更されたとしても、ロジックの変更はほとんどなしで済ますことができるのである。


自分のデータに対する均質イテレータ

さらに自分で作成した複数のデータを保持するデータクラスに対して、Enumeration を返すメソッドを与えておくのも、このイディオムの1つである。

class SomeObject {
     public Enumeration getEnumeration( ) {
        return new ArrayIterator( localObject );
     }
} 

.............
          SomeObject so = new SomeObject( .... );
          ..............
          ArrayIterator ai = so.getEnumeration();
          for( i = 0; ai.hasMoreElements(); i++ ) {
               System.out.println( i + ":" + ai.nextElement() );
          }

このようなかたちで、ArrayIterator は列挙を目的としたデータ交換のインターフェイスとしても使えるのである。

また、ArrayIterator は「現在どのデータを見ているのか」を示すインスタンス変数 counter を持っている。ということは、複数の ArrayIterator を同時に並行してスキャンさせることもできるのである。また、実装すれば「逆順のスキャン」なども可能になることはいうまでもない。

応用として、次のようなイテレータを宣言するのも使い勝手が良い。ライブラリのイテレータは汎用的に書かれているために、nextElement() が Object 型で定義されているが、具体的な利用の局面では、均質なデータを保持するデータを定義することの方がずっと多い。

interface MyDataIterator extends Enumeration {
    void rewind();  /* counter を初期値に戻す */
    void hasNext(); /* hasMoreElements() の代わり */
    MyData next();  /* nextElements() の代わりとして、
                     内容が均質(MyData)であることを保証する。*/
    void hasPrevious(); /* 一つ前が存在するか */
    MyData previous();  /* 一つ前を返す */
}

1.5で導入される Generic 型について

2004年内にリリースされる予定の J2SDK1.5 では、「Generic 型」の名称でいわゆる「パラメータ化クラス」とか「汎用体」「総称クラス」と呼ばれるものが導入される。これは、このイテレータと深く関わるので、ここで解説しよう。

今均質イテレータの話をした。この「均質イテレータ」の前提は、すべて同じクラスのインスタンスが格納された Collection(「均質Collection」)である。前のサンプルでは Enumeration インターフェイスを継承して、特化したクラスの戻り値を指定するメソッドを追加していたのだが、これってメリットはあるけど、実は馬鹿馬鹿しいことである。ようするに、フツーの Collection などを利用するときに、次のようなダウンキャストをするのだが、

LinkedList l = new LinkedList();
l.add( "test" );
/* とか色々 String オブジェクトだけを格納 */

String s = (String)l.get(0);

これが必要な理由は、add メソッドの引数はすべての基底クラスである Object 型だから...というものである。つまり、LinkedList(だけじゃなく Collection フレームワークのクラス)は、「とにかくどんなオブジェクトでも入る!」という観点で設計されているのだ。しかし、現実の利用の場合には、特定クラスのインスタンスだけが格納されて、それ以外のインスタンスが格納されるのはエラーとして検出したい..ということが多い。が、格納時には Object 型引数であるので、期待しない型であっても、格納できてしまうのである!

勿論、継承や委譲によって引数型を強制することは可能だが、「たかがそういう目的のために新しいクラスを作るのは、面倒な上に汎用的ではない」という判断で作らないことも多い。これを言語仕様の上で解決するのが、Generic型だ。

Generic型は「パラメータ化クラス」という別名を持つように、利用局面で「特定の型」をパラメータとして渡してインスタンスを生成し、その利用を指定した「特定の型」に限る、という機能を持っている。つまり、クラスとしての定義はどんな型でも受付可能な「総称型」であり、利用局面で「特定の型」をパラメータとして渡すことで、「具体的な型を持った現実のオブジェクト」になるのである。

こういうソースになるらしい。C++ のテンプレートを準用している。

List<String> list = new LinkedList<String>();
list.add( "test" );
String s = list.get(0);

あるいは、やはり 1.5 で導入される Autoboxing/Unboxing 機能(Integer 型などのプリミティブ型に対するラッパクラスに、本来のプリミティブ型を代入可能にする機能)から、今までは厳格にプリミティブ型とオブジェクト型が区別されていたのが曖昧に扱えるようになり、次のようにも書けるようだ。

List<Integer> list = new LinkedList<Integer>();
list.add( 5 ); /* 注意:リテラルの5はプリミティブ型 */
Integer val = list.get(0); /* さすがに取得はオブジェクト型 */

まあ、一応「プリミティブ型とオブジェクト型を差別せずに、同じクラスで扱える」という風な結果(中途半端でも...)が得られるようである。取得の側も、

List<int> list = new LinkedList<int>();
list.add( 5 ); /* 注意:リテラルの5はプリミティブ型 */
int val = list.get(0); /* 取得もプリミティブ型 */

もボクシング機能によって一応可能になっているようだ。このボクシング機能にせよ Generics 型にせよ、一種の「コードハック」によってバイトコード生成以前にソースを自動的に書き換えてコンパイルしている、という実装が何とも気色が悪いが、「均質Collection」がより安全なかたちで実現できる(add時にチェックできる)のは、喜ばしいことである。
(この項、技術評論社「JAVA PRESS Vol.35」、後藤大地「逆コンパイラを使ってJ2SE1.5の新機能を探る」を参照した)


Meyer の議論〜「手続き」と「関数」の分離について

Bertrand Meyer の「オブジェクト指向入門」で、このイテレータについて考察をしている部分がある。Meyer の開発した言語である Eiffel は、Pascal の子孫であるために、戻り値のある function と戻り値のない Procedure を区別している(この区別を破棄したのはC言語だが、Java はCのやり方を受け継いでいる)。いわゆる「関数型プログラミング」での「関数=function」の定義は、単に戻り値を返す/返さないではなくて、それが「副作用」(この場合にはオブジェクトの状態の変更程度の意味)を持つ/持たないでも区別される。つまり、次の通り。

関数 function
戻り値を持つ。そして、副作用がない。つまり、同一の関数は何度呼び出しても同じ値を返す。
手続き procedure
戻り値がない。しかし、副作用があっても良い。だから、関数呼び出しの間に手続き呼び出しが挟まった場合、関数呼び出しの戻り値が違うことがありうる。

この基準で言うと、Java の「ゲッタ」は関数であり、「セッタ」は手続きである。つまり、次の通り。

class SomeClass {
    int someValue = 0;

    int getSomeValue( void ) {   /* ゲッタ。関数である。 */
        return someValue;
    }

    void setSomeValue( int val ) { /* セッタ。手続きである。*/
        someValue = val;
    }
}

現状の Enumeration インターフェイスは、この区分に従っていない。hasMoreElements() は「関数」であるが、nextElement() は戻り値があって、しかも状態を変更している。だから nextElement() は関数とも手続きとも言えないものである。この区別をイテレータのインターフェイスに導入すると、次のようなインターフェイスになる。

public class ArrayIterator implements Enumeration {
     private Object [] array;
     private int counter = 0;

     public ArrayIterator( Object [] o ) { /* 配列に対するイテレータ */
          array = o;
     }

     /* hasMoreElements() を改名。こっちの方が適切 */
     public boolean isLast( ) {
          if( array.length >= counter ) {
               return false;
          }
          return true;
     }

     /* nextElement() の機能が分離される */
     public Object get( ) {   /* 関数にする */
          return array[counter]; 
     }

     public void forward( ) {  /* 次の element に進むのは手続きにする */
          counter++;
     }
}

まあ、これだけでは有難味は少しもないのだが、次のような手続きを追加することを考えてみよう。

  1. 何かのキーデータを検索し、その位置に移動する「手続き」
  2. 挿入や削除を実現する「手続き」
  3. 逆方向に進む「手続き」
  4. 先頭あるいは末尾にジャンプする「手続き」

このような場合には、「今の counter が示すオブジェクトを返して、counter を1つ進める」という nextElemet() インターフェイスの動作が不徹底なものであることがわかるだろう。それゆえ、counter の操作をする「手続き」と、現在の counter が示すデータを返す「関数」とは分離するのが相当である、というのが Meyer の主張である。筆者はこの Meyer の議論を正しいと思う。皆さんはいかがかな?

ちなみに Meyer は、均質イテレータの問題については、総称クラスを定義することを薦めている。これは総称クラスを持たない Java では難しいこと(細かいクラスが増えすぎる)であるが、このような解決もあるのだということを憶えておくと良いように思う(失礼!古い...)。全体的に Meyer の「オブジェクト指向入門」はこのようにアイデアに満ちた素晴らしい名著である。オブジェクト指向についてしっかり理解したいと考える皆さんは必読であるぞよ。



copyright by K.Sugiura, 1996-2006