The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目26:ジェネリック型を使用する

ジェネリック型を使う

今回は簡単にジェネリック型の使い方・書き方について説明します。

項目6での説明に利用したスタックの実装を、ジェネリックスを用いた実装に置き換えることを例としてみます。

ジェネリックスを利用しない元のスタック実装は以下の通りです。

static class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    public boolean isEmpty() {
         return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements,  2 * size + 1);
        }
    }
}

このスタック実装の問題は、要素を取り出す際に明示的な型キャストが必要になることです。

Stack stringStack = new Stack();
stringStack.push("2");
stringStack.push("1");

String one = (String) stringStack.pop();
// => キャストが必要
String two = (String) stringStack.pop();
// => キャストが必要

これに加えて、実行時にキャストが失敗する可能性すらあります。

Stack stringStack = new Stack();
stringStack.push("1");

Integer one = (Integer) stringStack.pop();
// => コンパイル可能だが実行時に例外(ClassCastException)が発生する。

ジェネリックスを使う

このスタックをジェネリックスを利用した実装に変更してみます。

最初のステップは、このクラスに要素を表す1つの型パラメータ E を追加することです。 そして、Object を使用しているすべての箇所を型パラメータ E に置き換えます。

static class Stack<E> { // 変更
    private E[] elements; // 変更
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY]; // 変更
    }

    public void push(E e) { // 変更
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() { // 変更
        if (size == 0) {
            throw new EmptyStackException();
        }

        E result = elements[--size]; // 変更
        elements[size] = null;
        return result;
    }

    // ...
    // isEmpty() と ensureCapacity() は同じ
}

しかし、項目25で解説したように、具象化不可能型である E の配列は生成できません。 そのため、コンパイルエラーとなります。

public Stack() {
    elements = new E[DEFAULT_INITIAL_CAPACITY];
    // => コンパイルエラー。E[] は生成できない。
}

これに対して Object[] を生成するように変更し、E[] 型にキャストするようにします。 すると、代わりに無検査警告が発生するようになります。

public Stack() {
    elements = (E[]) new E[DEFAULT_INITIAL_CAPACITY];
    // => 無検査キャストのコンパイル警告
}

コンパイラはこのコードの型安全性を保障することができないため、無検査キャストを警告しています。

しかし、実際にコードを見てみると elements フィールドに要素を挿入するのは push() だけであり、その際の要素は E 型であることが分かります。 加えて、elements フィールドは private でありクラスの外に参照を渡していません。

そのため、項目23で説明したように、アノテーションによる警告を抑制することが正当化されます。

// elements 配列は push() からのみ値を挿入されるため、問題なし
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

第2の方法

さきほど挙げた、E[] は生成できないというコンパイルエラーを解決する方法がもう一つあります。

それは、elements フィールドの型を Object[] にしてしまう方法です

static class Stack<E> {
    private Object[] elements;
    // ... 省略 ...
} 

この結果、コンストラクタでのコンパイルエラーは発生しなくなりますが、pop() でコンパイルエラーが発生するようになります。

public E pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }

    E result = elements[--size];
    // => コンパイルエラー。互換性のない型。
    // => Object インスタンスを E 型の変数に代入しようとしている
    elements[size] = null;
    return result;
}

これに対しては要素を E 型へキャストすることで解決できます。 同じように無検査警告が発生するようになりますが、elements フィールドには E 型の要素のみが含まれることが保障できるので、警告を抑制するべきです。

// elements は push メソッドのみで挿入され、要素は E 型のみであることが保障される
@SuppressWarnings("unchecked")
E result = (E) elements[--size];

ジェネリックス配列の生成エラーに対処するためには、この2つの技法のどちらかを使うしかありません。

一般的に、スカラ型の無検査キャストを抑制するほうが、配列型の無検査キャストを抑制するよりも、危険性は低いです。 ただし、「第2の方法」で示したやり方では、コード上のいたるところで警告を抑制することになるため、現実には最初の方法がよく利用されます。

配列よりリストを使う?

今回の例ではジェネリックス版スタックの実装に配列を用いています。 これは項目25:配列よりリストを使うの提言と矛盾しているようにも見えます。

しかし、実際には Java はリストを直接サポートしていませんので、どこかでは必ず配列を利用して実装しなければなりません。

また、パフォーマンスなどの理由によって配列が用いられることもあります。 例えば HashMap ではパフォーマンスのために配列を利用して実装されています。

境界型パラメータ

一般的に、ジェネリックス型の型パラメータには制約がありません。 Stack<Object>、Stack<int[]>、Stack<List<String>> など、いろいろなジェネリックススタックを生成できます。

ただし、基本データ型のパラメータ化された型は生成できません。Stack<int> や Stack<double> は生成できないのです。 これは Java の根本的な問題ですが、ボクシングされたデータ型を使うとこの制限を回避できます。

型パラメータに使用できる型を制限する機能もあります。

class DelayQueue<E extends Delayed> implements BlockingQueue<E> {
   ...
}

このように型パラメータに extends キーワードを用いると、実型パラメータが Delayed のサブタイプでなければならないことを要求できます。

この型パラメータは境界型パラメータと呼ばれています。 これによって、DelayQueue ではキャストの必要性や ClassCastException の危険性を考えることなく、要素 E に対し Delay 型の持つメソッドを利用できます。

なお、すべての型は自身のサブタイプであると定義されていますので、Delayed 自体も利用することができます。

感想

特になし。

(c) The King's Museum