キュー構造をJavaで実装してジェネリック型を理解する【改訂版】Eclipseではじめるプログラミング(19)(2/3 ページ)

» 2010年10月14日 00時00分 公開
[小山博史株式会社ガリレオ]

【第1改訂】Objectクラスを使ってQueueを実装

 先ほどのコードでは、プログラムの対象としているのがint型であり、メソッドやフィールドにint型がよく出てきていることに注目してください。

 このプログラムをlong型のデータも扱うためのプログラムへ変更するには、どうすれば良いか考えてみましょう。キューのような有名なデータ構造を利用するプログラムはよくあるので、int型だけでなく、ほかのデータ型についても簡単に利用できるようにしたくなるものですね。

扱うデータ型が増えるたびに新クラスを用意!?

 Queueクラスを変更して、long型データも対象とするクラスにするために、すぐ思い付く方法は、「int」と書いてある個所を、すべてlongへ置き換えた「LongQueue」クラスを別途用意する方法でしょう。そういわれると、「えー」と思う読者も多いことでしょう。これでは、新しいデータ型を扱うQueueクラスが必要になるたびに、新しいクラスを作成する必要が出てきて大変ですからね。

データ型としてObject型を使ってみる

 やはり、「処理の変更が必要ないなら、ソースコードを書き換えないで対応できないか」と思うはずです。そこで次に思い付くのが、データ型としてObject型を使うという方法です。Javaでは、Object型はすべてのクラスのスーパークラスですし、「基本データ型プリミティブ型)」についても「ラッパークラス」により対応ができます。

 ということで、Object型を使ったプログラムへ変更してみましょう。変更点は次のとおりです。コード上で変更があった行には「//」を付けてあります。

  • int型をObject型に変更
  • キューには、null以外の値を入れることができるように変更
  • dequeueでキューが空のときに「-1」ではなく「null」を返すように変更
  • mainメソッドでenqueueを使うときにint型の値ではなくlong型の値を指定するように変更
package sample19;
 
public class Sample02 {
    static class Queue {
        final int SIZE = 5;
        private Object[] values = new Object[SIZE+1]; //
        private int head = 0;
        private int tail = 0;
        boolean enqueue(Object data) { //
            if (data == null) return false;
            if (((tail + 1) % values.length) == head) {
                return false;
            }
            values[tail++] = data;
            tail = tail % values.length;
            return true;
        }
        Object dequeue() { //
            Object data = null; //
            if (tail != head) {
                data = values[head++];
                head = head % values.length;
            }
            return data;
        }
        boolean isEmpty() {
            return (tail == head);
        }
    }
 
    public static void main(String[] args) {
        Queue q = new Queue();
        q.enqueue(1L); //
        q.enqueue(2L); //
        q.enqueue(3L); //
        q.enqueue(4L); //
        q.enqueue(5L); //
        q.enqueue(6L); //
        System.out.println(q.dequeue());
        q.enqueue(7L); //
        while (!q.isEmpty()) {
            long data = (Long)q.dequeue(); //
            System.out.print(data+",");
        }
        System.out.println("");
    }
}
sample19/Sample02.java

「型変換」を駆使するも……

 ここで、データを取り出すときの処理に注目してください。前回の「型変換」で学んだ「キャスト」を使っています。また、Long型からlong型へは「アンボクシング」によって暗黙のうちに型変換されています。

 型について安全でない「ダウンキャスト」を使うことになるので、Object型を使ったキューのプログラムでは、キューへ代入した型について気を付けながらプログラミングをする必要が出てきます。実行結果は変わらないので省略します(以降、同様です)。

安全でない「ダウンキャスト」が気になる

 さて、このプログラムは意図したとおりに動作しますが、ダウンキャストしている点が気になります。いろいろな型のデータをキューへ入れたい場合には、Object型を扱うキューは便利なのですが、同じ型のデータだけをキューへ入れたい場合に、もっと良い方法はないのでしょうか。

 こんなときのために、ジェネリックスというものが用意されているので、これを使ってみましょう。

ジェネリック型を使うための基礎知識

 ジェネリックスの機能を利用する場合は、「ジェネリック型(generic type)」としてクラスを宣言します。Eclipseでは、「generic type」のことを「総称型」と訳しているようで、Javaのコードを編集しているときに出る警告やコンパイル時のエラーでは、総称型という用語が出てきます。ここでは、ジェネリック型という用語を使います。

ジェネリック型の使い方

 ジェネリック型のAクラスを定義するには、次のコードのようにします。

class A<T> {
    T field1;
    void method1(T t) {
 
    }
}

「型変数」「型パラメータ」

 「T」は、「型変数(type variable)」といいます。「class A」のように「」として記述される型変数「T」は「パラメータとして型を使う」ことを宣言しているので、「型パラメータ(type parameter)」といいます。

 変数名は「T」でなくても構いません。「E」や「V」や「ElementType」など、ほかの文字列も使えます。1文字で指定することが多いようです。Aクラスの定義内で使われる型変数「T」は、Aクラスを利用するときに指定される型に置き換わります。

「型引数」

 Aクラスを使う場合、型パラメータ「T」に対応する「型引数(type argument)」を指定します。次の例では、具体的なクラス名として「String」を指定しています。これにより、先ほどのAクラスで「T」と指定されていた型がjava.lang.Stringとなります。

A<String> a;

「パラメータ化された型」

 「A」のように指定された型を、「パラメータ化された型(parameterized type)」といいます。「パラメータ付きの型」ということもあります。

 ジェネリック型に対して型引数を指定せずに使うこともできます。この場合の型を、「原型(raw type)」といい、通常はコンパイラが警告を出します。例えばEclipseでは「Aはraw型です。総称型Aへの参照はパラメータ化する必要があります」といった警告が出ます。

A a;

【第2改訂】Queueをジェネリックスの機能を使って実装

 具体的にQueueをジェネリックスの機能を使って実装するとどうなるか、次のコードを見てみましょう。

package sample19;
 
public class Sample03 {
    static class Queue<T> { //
        final int SIZE = 5;
        private Object[] values = new Object[SIZE+1];
        private int head = 0;
        private int tail = 0;
        boolean enqueue(T data) { //
            if (data == null) return false;
            if (((tail + 1) % values.length) == head) {
                return false;
            }
            values[tail++] = data;
            tail = tail % values.length;
            return true;
        }
        T dequeue() { //
            T data = null; //
            if (tail != head) {
                data = (T)values[head++]; //
                head = head % values.length;
            }
            return data;
        }
        boolean isEmpty() {
            return (tail == head);
        }
    }
 
    public static void main(String[] args) {
        Queue<Long> q = new Queue<Long>(); //
        q.enqueue(1L);
        q.enqueue(2L);
        q.enqueue(3L);
        q.enqueue(4L);
        q.enqueue(5L);
        q.enqueue(6L);
        System.out.println(q.dequeue());
        q.enqueue(7L);
        while (!q.isEmpty()) {
            long data = q.dequeue(); //
            System.out.print(data+",");
        }
        System.out.println("");
    }
}
sample19/Sample03.java

 Queueクラスの宣言時に、型変数として「T」を指定している点に注目してください。また、enqueueメソッド、dequeueメソッドでも型変数を使った宣言になっている点にも注目してください。変更があった行には「//」を付けてあります。

ジェネリックスの機能を使うメソッド

 使うときは、Sample03クラスのmainメソッドのようになります。Long型のデータを扱うキューとして「Queue<Long> q」のように記述します。インスタンス生成時にも「new Queue<Long>();」と型を指定します。

 ジェネリックスを使うことで、Sample02クラスでは「long data = (Long)q.dequeue();」としてダウンキャストが必要だった処理が「long data = q.dequeue();」のように簡潔に記述できるようになっています。

 ただし、利用する場合のダウンキャストは必要なくなりましたが、Queueクラスの中でダウンキャストが必要となっている点を見落とさないようにしてください。

ジェネリックスで、型について安全になった!

 次に、Integer型のデータを扱うキューとしてSample03.Queueクラスを使ってみましょう。Sample03.Queueクラス側へ変更を加える必要はなく、Sample04クラスのように利用できます。

package sample19;
 
public class Sample04 {
    public static void main(String[] args) {
        Sample03.Queue<Integer> q = new Sample03.Queue<Integer>();
        q.enqueue(1);
        q.enqueue(2);
        q.enqueue(3);
        q.enqueue(4);
        q.enqueue(5);
        q.enqueue(6);
        System.out.println(q.dequeue());
        q.enqueue(7);
        while (!q.isEmpty()) {
            int data = q.dequeue();
            System.out.print(data+",");
        }
        System.out.println("");
    }
}
sample19/Sample04.java

 どうでしょうか。ジェネリック型を使うと、Queueへ入れるデータ型を指定できるので、Object型を使うよりも型について安全になることが分かったと思います。

 こういったジェネリックスの便利さが分かってくると、どんどんと使いたくなるはずです。さらにジェネリックスを使いこなすために、次ページでは、Java APIのジェネリック型やワイルドカードについて説明します。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。