読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目29:型安全な異種コンテナーを検討する

今回は「型安全な異種コンテナーを検討する」だ。

ジェネリックスの章の最後の項目だけあって比較的発展的な内容。

異種コンテナー

通常、ジェネリックスは Set<Integer> や List<String> のように、特定の型の要素を持つコンテナーに対して利用します。

List<String> list = new ArrayList<>();
// => List<String> は String 型の要素をもつコンテナ

Set<Integer> set = new HashSet<>();
// => Set<Integer> は Integer 型の要素をもつコンテナ

これは、たとえ Map のような複数の型パラメータを持つ場合でも、型が固定されていることには変わりありません。

Map<Integer, String> map = new HashMap();
// => 「キー:Integer、値:String 型」のコンテナ

本項目では様々な型の値を保持することができる異種コンテナをとりあげ、ジェネリックスに対する理解を深めます。 例として「型」自体をキーとして、その型のインスタンスを保持する Map を実装してみます。

このような Map はどのような場合に有用でしょうか。 たとえば、「データベースにおけるテーブルの一行」を表すクラスのような場合、列には様々な型の値が含まれます。 このクラスを、さまざまな型の値を保持するできる Map で実装すると大いにメリットがあります。

Class クラス

型を Map のキーとして扱うためには Class クラスを利用します。

Class クラスは Java の「クラス」それ自体の情報を保持するためのクラスです。 Object クラスには getClass() というメソッドがあり、このメソッドを介して実行時におけるインスタンスの「クラス」を取得することができます。

実際のコードは以下のようになります。

String name = "The King's Museum";
Class stringClazz = name.getClass();
// => stringClazz は「String クラス」という情報を保持している
// 「class」という文字列は構文上、キーワードになるので clazz という変数名がよく利用される

stringClazz.getName(); 
// => "java.lang.String"

Integer one  = new Integer(1);
Class integerClazz = one.getClass();
integerClazz.getName(); 
// => "java.lang.Integer"

クラスリテラル

Class のインスタンスは Object.getClass() に加えて クラス名.class として取得することもできます。 これをクラスリテラルと呼びます。

Class stringClazz = String.class;
stringClazz.getName(); 
// => "java.lang.String"

Class integerClazz = Integer.class;
// => "java.lang.Integer"

ここまで Class クラスをジェネリックスなしで使ってきましたが、実は Class クラスはジェネリックスクラスです。 保持する対象となるクラスを型パラメータとして与えることができ、コンパイル時に型チェックが行われるようになります。

Class<String> strngClazz = String.class;
// => 問題なし

Class<Integer> integerClazz = String.class;
// => コンパイルエラー
// String.class は Class<String> 型である

クラスリテラルは具象化不可能型には利用できません。 すなわち、String.class、String[].class は利用できますが、List<String>.class や List<Integer>.class はコンパイルエラーとなります。

Class<List<String>> clazz;
// => これは問題ない。宣言はできる

Class<List<String>> clazz = List<String>.class;
// => コンパイルエラー

List<String> も List<Integer> も実行時は List として扱われるため Class インスタンスは共有されることになります。 もし、List<String>.class や List<Integer>.class を言語として許可してしまうと、言語の利用者は混乱してしまうでしょう。

型安全な異種コンテナー

この Class クラスのインスタンスを用いて「クラス」自体をキーにして Map を作ることができます。 クラスをキー、そのクラスのインスタンスを値とする異種コンテナーのインタフェースは以下のようになります。

public interface Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

Class<T> クラスをキー、型パラメータ T のインスタンスが値となっています。 実際に、このコンテナを使うコードは下記のようになります。

Favorites favorites = new Favorites();
favorites.putFavorite(String.class, "Java");
favorites.putFavorite(Integer.class, new Integer(1));
String favorite = favorites.getFavorite(String.class);

クラスリテラルコンパイル時・実行時の型情報を渡すために渡された場合、それを型トークンと呼びます。

このコンテナは型安全です。 すなわち、String 型のインスタンスを要求したときに、Integer インスタンスを返すことはありません. このように複数の型のインスタンスを保持し、かつ型安全が保障されるコンテナを、型安全異種コンテナーと呼びます。

異種コンテナーの実装

では、実装を見てみます。

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null) {
            throw new NullPointerException("Type is null");
         }
         favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

実際にキーと値の関係を保持しているのは HashMap<Class<?>, Object> 型の favorites プロパティです。

キーは Class<?> 型で値が Object 型です。 値が Object であるということは、Map に put した瞬間にキーと値の型関係はなくなることを意味します。 ただし、キーに Class インスタンスを保持しているので、get の際に型を復元することができます。

get の際には Object を T にキャストする必要があります。 ただし、単純に (T) value; としてキャストしてしまうと、無検査キャスト警告が発生してしまいます( 項目24)。

そこで、Class クラスの cast メソッドを使いキャストを行います。 Class クラスの cast は実行時の型をチェックして、キャスト不可能な場合は ClassCastException を発生させます。

しかし、この Map における put メソッドは、コンパイラによってキーと値が同じ型関係を持つことが保障されています。 加えて、この Map は外部から変更することができないため、キーと値の型に関する不変条件を破ることはできません。

悪意のあるクライアントは原型を利用して put するかもしれません(コンパイル警告は発生します)。 そのようなクライアントを防ぐためには put 時に Class.cast を利用できます。

public <T> void putFavroite(Class<T> type, T instance) {
    favorites.put(type, type.cast(instance));
    // => cast() メソッドを利用することで実行時に型チェックが可能になる
} 

このように実装すると、原型を利用して無理矢理異なる型のオブジェクトを put しようとした場合、実行時に ClassCastException が発生するようになります。

このテクニックを使った実装の実例として java.util.Collections の checkedSet、checkedList、checkedMap が挙げられます。 これらのクラスは、値の挿入時に実行時の型チェックを行い、正しい型の値がコンテナに含まれることを保障します。

境界型トークン

上記の Favorites では、キーとして使える型トークンの型に特に制限はありませんでした。 もし、型を制限したい場合には境界型トークンを使います。 特に Javaアノテーション API はこの境界型トークンを広範囲に利用しています。

例えば、アノテーションを付与できる要素(クラスやメソッド)は AnnotatedElement インタフェースを実装しています。 同時に AnnotedElement インタフェースは、付与されたアノテーションの種類をキーとして、アノテーションインスタンスを取りだすメソッド getAnnotation を持っています。

すなわち、AnnotatedElement を実装するクラスは型トークンをキーとする異種コンテナーになります。 そして、その型トークンは Annotation のスーパータイプのクラス、Class<T extends Annotation> であると制限されています。

実際の getAnnotation のインタフェースは以下のようになっています。 型パラメータ T が Annotation のサブクラスであることが要求されています。

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

感想

Class クラスの cast については少し思うところがあったのだが、長くなったのでとりあえず次回に持ち越しかな。。。