はじめに
私の前回の記事では、J2SE 5.0の新しいコレクションの機能を用いて、コレクションで使用する特定の型を指定する方法を説明しました。また、新たに導入されたfor each
構文を利用すれば、「反復子(イテレータ)」を使わずにコレクションを参照できることも説明しました。しかし、前回の記事で紹介したのは、コレクションの新機能の半分でしかありません。この記事では、J2SEの最新機能に適合したコレクションを作る方法を紹介します。
ジェネリックをサポートするクラスの作成
まず、「ジェネリック型」をサポートするクラスの作り方を学ぶ必要があります。「ジェネリック型をサポートする」とは、クラスのインスタンス作成を行うたびに、そのインスタンスに関連付けるJavaの型を1つ以上指定できることを意味します。これを説明するために、リスト1の簡単なサンプルクラスについて考えてみましょう。
package com.heatonresearch.examples.collections; public class Example<ONE, TWO, THREE> { private ONE one; private TWO two; private THREE three; public ONE getOne() { return one; } public void setOne(ONE one) { this.one = one; } public THREE getThree() { return three; } public void setThree(THREE three) { this.three = three; } public TWO getTwo() { return two; } public void setTwo(TWO two) { this.two = two; } public static void main(String args[]) { Example<Double, Integer, String> example = new Example<Double, Integer, String>(); example.setOne(1.5); example.setTwo(2); example.setThree("Three"); } }
リスト1のクラスがどのように宣言されているかに注目してください。最初の部分にある山かっこ内に3つのジェネリックが記されています。これらのジェネリックは、実際の型が入るプレースホルダです。この種のクラスをインスタンス化するときは、"ONE"、"TWO"、および"THREE"の代わりとなる具体的なクラス名を指定できます。指定を行わない場合、このクラスはデフォルトのオブジェクト型を使用します。
次に示すのは、Example
型クラスのインスタンスを作成する方法です。
Example<Double, Integer, String> example
= new Example<Double, Integer, String>();
上記のコードは、指定したDouble
、Integer
、String
の各型をリスト1の"ONE"、"TWO"、"THREE"の各プレースホルダにあてはめます。各変数に値を設定する次の3行のコードを見ると、それぞれの変数が指定どおり別々の型を持つことがわかります。
example.setOne(1.5);
example.setTwo(2);
example.setThree("Three");
これで、ジェネリックを用いたカスタムクラスの作り方がわかりました。この点を押さえておけば、ジェネリックを利用するカスタムのコレクションクラスを作るのはもっと簡単です。
キュークラスの作成
キューというデータ構造が非常に役に立つ場合があります。キューの機能を理解するために、遊園地で乗り物の順番を待つ人々の列を想像してみましょう。新たに列に加わる人は、列の最後尾に並びます。順番を待っていれば、いつかは列の先頭にたどり着きます。並んでいる人々の順序が入れ替わることはありません。
これと同じ原理が、キュークラスにもあてはまります。キュークラスには、push
とpop
という2つのメソッドがあります。キューにオブジェクトを入れるにはpush
メソッドを、キューからオブジェクトを取り出すにはpop
メソッドを使います。たとえば、push
メソッドを使って3つのオブジェクトをキューに追加した場合、pop
メソッドを3回呼び出せば、この3つのオブジェクトは追加したときと同じ順序で取り出されます。遊園地の順番待ちの列と同じです。ある順序で列に並んだ3人は、同じ順序で乗り物にたどり着くわけです。
次のコードは、Javaによる、ジェネリックを利用したキューの実装方法を示しています。
package com.heatonresearch.examples.collections; import java.util.*; public class Queue<T> { private ArrayList<T> list = new ArrayList<T>(); public void push(T obj) { list.add(obj); } public T pop() throws QueueException { if (size() == 0) throw new QueueException( "Tried to pop something from the queue, " + "when it was empty"); T result = list.get(0); list.remove(0); return result; } public boolean isEmpty() { return list.isEmpty(); } public int size() { return list.size(); } public void clear() { list.clear(); } }
上記のコードでは、このキュークラスのためにジェネリック型を1つ使うことを宣言しています。
public class Queue<T>
ジェネリック型T
は、キューに格納されるクラスの型です。キューにアイテムを格納するために、このクラスはT
型を受け取るArrayList
も作成します。
このクラスのpush
メソッドは非常にシンプルです。ジェネリック型T
のオブジェクトを1つ受け取り、ArrayList
に追加するだけです。
pop
メソッドの方はもう少し複雑です。まず、キューからオブジェクトを1つポップしようとしたときにオブジェクトが存在しなければ、このクラスはQueueException
型の例外をスローします。次に示すのが、QueueException
クラスです。
package com.heatonresearch.examples.collections; public class QueueException extends Exception { public QueueException(String msg) { super(msg); } }
QueueException
をスローするコードは次のようになります。
if (size() == 0) throw new QueueException( "Tried to pop something from the queue, " + "when it was empty");
キューが空でない場合、このメソッドはキューから最後の要素を取り出し、result
という変数に格納したうえで、この要素をキューのリストから削除します。この処理を実装したのが、次のコードです。
T result = list.get(0);
list.remove(0);
return result;
一時的な変数result
もまた、ジェネリック型T
のメンバになっていることに注意してください。ジェネリック型に実際のJavaの型を「代入」してこのクラスを使うときは、互換性のレベルを最大化するために、こうした変数の参照には必ずジェネリック型を使うことが重要です。
キュークラスの動作テスト
次のクラスは、「ジェネリックな」キューの動作をテストするためのものです。
package com.heatonresearch.examples.collections; public class TestQueue { public static void main(String args[]) { Queue<Integer> queue = new Queue<Integer>(); queue.push(1); queue.push(2); queue.push(3); try { System.out.println("Pop 1:" + queue.pop()); System.out.println("Pop 2:" + queue.pop()); System.out.println("Pop 3:" + queue.pop()); } catch (QueueException e) { e.printStackTrace(); } } }
上記のコードで作られるキューは、Integer
オブジェクトしか受け取りません。
Queue<Integer> queue = new Queue<Integer>();
このテストでは、次に3つの整数をキューに追加します。
queue.push(1); queue.push(2); queue.push(3);
キューに追加された数がプリミティブ型であることに注意してください。J2SEのオートボクシング機能のおかげで、プリミティブなint
型が自動的にInteger
オブジェクトに変換されるのです。
次に、pop
メソッドを使ってこのオブジェクトを取り出します。キューが空の場合は、QueueException
をキャッチします。このキューから3つの数をポップした結果は次のとおりです。
1 2 3
ここではInteger
オブジェクトを受け取るキューを紹介しましたが、このキュークラスはジェネリック型なので、どんなJavaオブジェクトに対しても動作します。
先読み可能なスタックコレクションの作成
ここでは、もっと複雑なコレクション型として、オブジェクトを実際に取り出す前に先読みが可能な(つまり、「こっそり参照できる」)スタックをとりあげます。反復子またはJ2SE 5.0の新しいfor each
構文を使えば、先読みができます。
ここで紹介するPeekableStack
クラスは、先入れ後出し(FILO)方式のスタックであり、現在のスタックの中身に対する繰り返し処理を伴います。実装には2つのクラスを使用します。1つ目は、実際にスタックを実装するPeekableStack
クラスです。2つ目は、「Java標準」のIterator
クラスを実装するPeekableStackIterator
クラスで、スタックに対する繰り返し処理を可能にします。リスト2は、PeekableStack
クラスです。
package com.heatonresearch.examples.collections; import java.util.*; public class PeekableStack<T> implements Iterable<T> { private int version; private ArrayList<T> list = new ArrayList<T>(); public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } public Iterator<T> iterator() { PeekableStackIterator peekableStackIterator = new PeekableStackIterator(this, list); return peekableStackIterator; } public void push(T obj) { version++; list.add(obj); } public T pop() { // find the last element int last = list.size() - 1; // is the stack empty? if (last < 0) return null; // return the last element and remove it T result = list.get(last); list.remove(last); return result; } public int size() { return list.size(); } }
リスト2のPeekableStack
クラスは、Iterable
インターフェイスを実装していることに注意してください。このインターフェイスは、J2SE 5.0の新しいfor each
構文をサポートするために必要になります。Iterable
インターフェイスにより、コレクションがiterator
メソッドをサポートしていることを明示します。iterator
メソッドは反復子を返すものです。このインターフェイスがなければ、使用するクラスはfor each
構文に対応できません。
先読み可能なスタックには、先ほどのキューと同じようにpush
とpop
の両メソッドが含まれています。push
メソッドは、キューのときより少しだけ複雑になっています。このpush
メソッドでは、スタックにオブジェクトを追加するとversion
という変数の値をインクリメントします。
このversion
変数によって、PeekableStackIterator
クラスがスタックに変更を加えることを防止できます。反復子を作成すると、現在のバージョン番号のコピーがその反復子によって保持されます。push
メソッドの呼び出しによって、スタックになんらかの変更が生じると、バージョン番号が一致しなくなります。この不一致が起こると、反復子はConcurrentModificationException
をスローします。
pop
メソッドはもう少し複雑です。最初に、このメソッドは取得したリストのサイズから1を引くことによって、リストの最後の要素のインデックスを求めます。
int last = list.size() - 1;
この結果が0未満の場合は、スタックが空なので、pop
メソッドはnull
を返します。
if (last < 0) return null;
スタックに「最後の要素」が存在する場合は、リストからその要素を取り出します。この要素は、リストからの取り出しに成功した後で削除できます。
T result = list.get(last); list.remove(last);
最後に、リストから取り出したオブジェクトを返します。
return result;
for each
による繰り返しをサポートするために、PeekableStack
クラスのiterator
メソッドは、「Java標準」のIterator
クラスを返します。Iterator
クラスを使えば、スタックに含まれるすべてのオブジェクトに対する繰り返し処理が可能です。このiterator
メソッドは、新しい反復子を作成して返します。
PeekableStackIterator peekableStackIterator = new PeekableStackIterator(this, list);
ご覧のとおり、この反復子のクラスは、コンストラクタの引数として現在のスタックとスタックが持つ要素のリストを受け取ります。これらの値は、PeekableStackIterator
クラスによって使用されるのですが、これについては次のセクションで説明します。
先読み可能スタックの反復子の作成
Javaの新しいfor each
構文を用いてPeekableStack
クラスを利用する場合、「Java標準」の反復子を作成する必要があります。リスト3にPeekableStackIterator
クラスの実装を示します。
package com.heatonresearch.examples.collections; import java.util.*; public class PeekableStackIterator<T> implements Iterator { private int version; private PeekableStack<T> source; private List<T> list; private int position; PeekableStackIterator(PeekableStack<T> source, List<T> list) { this.source = source; this.list = list; this.version = source.getVersion(); this.position = 0; } public boolean hasNext() { if (version != source.getVersion()) throw new ConcurrentModificationException(); return (position < list.size()); } public T next() { // check for concurrent modification if (version != source.getVersion()) throw new ConcurrentModificationException(); // if its empty then return null if (!hasNext()) return null; // find the current position int last = list.size() - 1; last -= position; // if there are still elements left, return the // next one and update the current position. T result = null; if (last >= 0) { position++; result = list.get(last); } return result; } public void remove() { } }
リスト3の反復子が、スタックの値を実際に変更することは一切ありません。その代わりに、要素のリストにおける現在の位置を絶えず把握し、常に、その次の位置にある要素を返します。現在の要素の位置についての情報を反復子のクラス自体が保持しているので、同じスタックに対して複数の反復子を利用することも可能です。
次のプログラムは、先読み可能スタックの動作をテストするためのものです。
package com.heatonresearch.examples.collections; import java.util.*; public class TestPeekableStack { public static void main(String args[]) { PeekableStack<Integer> stack = new PeekableStack<Integer>(); stack.push(1); stack.push(2); stack.push(3); for (int i : stack) { System.out.println(i); } System.out.println("Pop 1:" + stack.pop()); System.out.println("Pop 2:" + stack.pop()); System.out.println("Pop 3:" + stack.pop()); } }
ご覧のとおり、スタックに3つの要素を追加しています。その後、新しいfor each
構文を用いて、この3つの要素の値を表示しています。
for( int i: stack) { System.out.println( i ); }
この記事では、J2SEの新仕様であるジェネリックとfor each
構文をサポートしたコレクションを、そつなく実装する方法を紹介してきました。ご覧いただいたように、ジェネリックの活用と適切なインターフェイスの実装により、J2SE 5.0の新しい構文に対応したコレクションを作成するのは比較的容易です。こうしたコレクションクラスがシームレスに統合されてJ2SE 5.0に導入されていることがおわかりになったと思います。