SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

J2SE 5.0を使ったカスタムジェネリックコレクションの作成

ジェネリックとfor each構文をサポートするコレクションの実装

  • このエントリーをはてなブックマークに追加

J2SE 5.0のコレクションAPIには、注目すべき機能が数多く追加されています。この記事では、複数の型や新しいfor each構文にシームレスに対応する、カスタムなジェネリックコレクションを実装するために知っておくべき機能を紹介します。

  • このエントリーをはてなブックマークに追加

はじめに

 私の前回の記事では、J2SE 5.0の新しいコレクションの機能を用いて、コレクションで使用する特定の型を指定する方法を説明しました。また、新たに導入されたfor each構文を利用すれば、「反復子(イテレータ)」を使わずにコレクションを参照できることも説明しました。しかし、前回の記事で紹介したのは、コレクションの新機能の半分でしかありません。この記事では、J2SEの最新機能に適合したコレクションを作る方法を紹介します。

ジェネリックをサポートするクラスの作成

 まず、「ジェネリック型」をサポートするクラスの作り方を学ぶ必要があります。「ジェネリック型をサポートする」とは、クラスのインスタンス作成を行うたびに、そのインスタンスに関連付けるJavaの型を1つ以上指定できることを意味します。これを説明するために、リスト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>();

 上記のコードは、指定したDoubleIntegerStringの各型をリスト1の"ONE"、"TWO"、"THREE"の各プレースホルダにあてはめます。各変数に値を設定する次の3行のコードを見ると、それぞれの変数が指定どおり別々の型を持つことがわかります。

example.setOne(1.5);
example.setTwo(2);
example.setThree("Three");

 これで、ジェネリックを用いたカスタムクラスの作り方がわかりました。この点を押さえておけば、ジェネリックを利用するカスタムのコレクションクラスを作るのはもっと簡単です。

キュークラスの作成

 キューというデータ構造が非常に役に立つ場合があります。キューの機能を理解するために、遊園地で乗り物の順番を待つ人々の列を想像してみましょう。新たに列に加わる人は、列の最後尾に並びます。順番を待っていれば、いつかは列の先頭にたどり着きます。並んでいる人々の順序が入れ替わることはありません。

 これと同じ原理が、キュークラスにもあてはまります。キュークラスには、pushpopという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クラスです。

リスト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構文に対応できません。

 先読み可能なスタックには、先ほどのキューと同じようにpushpopの両メソッドが含まれています。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クラスの実装を示します。

リスト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に導入されていることがおわかりになったと思います。

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Jeff Heaton(Jeff Heaton)

ライター、人工知能(AI)研究者、元大学教員。AI、仮想世界、スパイダー、ボットなどの話題を取り上げて執筆した書籍は10冊以上。Java、.Net、Silverlightを対象に、高度なニューラルネットワークおよびAIフレームワークの提供を目的とするオープンソースイニシアチブ、Encogプロジェクトを統括している。また、個人のWebサイトを管理し、人工知能とスパイダー/ボットプログラミングをはじめとする話題について情報発信を行っている。メールの宛先はjheaton@heatonresearch.com。Sun認定Javaプログラマ兼IEEEシニアメンバー。ミズーリ州セントルイスのワシントン大学情報管理修士号を持ち、セントルイス在住。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/209 2006/01/06 20:38

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング