33.型安全な異種コンテナーを検討する
Set<E>
やMap<K,V>
といったcollections、また、ThreadLocal<T>
やAtomicReference<T>
といった要素を格納するコンテナでは、型パラメータの数が固定されている(Setだと1つ、Mapだと2つ)。
型パラメータの数が固定されていることは、大概の場合において、ユーザが望んでいることだが、時にはもっと柔軟性が必要となるときがある。こういった時の対応方法としては、コンテナをパラメータ化するのではなく、キーをパラメータ化する。
このアプローチのシンプルな例として、クライアントに任意で複数の型のインスタンスを格納、検索をさせるFavorites
クラスを考える。
Favorites
クラスのAPIは以下のよう。MapのキーがClass<T>
になっている。
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
以下のコードを実行すると、
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,
favoriteInteger, favoriteClass.getName());
}
予想通り、Java cafebabe パッケージ名$Favorites と出る。
Favorites
インスタンスはタイプセーフであり、異種(heterogeneous:普通のMapと異なり、全てのキーの型が異なる)であるため、Favorites
のようなクラスは、typesafe heterogeneous container と呼ばれる。
実装は以下のよう。
// Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
MapのキーがClass<?>
となっており、どんな型でもキーに取ることができる。
また、MapのバリューはObject
である。つまり、Mapにおいてはキーとバリューの型の結びつきは保証されていない。
putFavorites
メソッドにおいては、単純にClassオブジェクトとインスタンスをMapに入れ込むのみ。
getFavorites
メソッドは技巧的で、Classオブジェクトをキーに、Mapから取ってきたバリューを、Classのcast
メソッドで動的にT
を返すようにキャストしている。
cast
メソッドは、引数に取ったインスタンスが自身と同じ型なら、自身の型にキャストして戻す、ということをしている。
Favorites
クラスには2つ気を付けるべきことがある。
1つ目は、悪意のあるクライアントは、rawタイプなClassオブジェクトを使ってFavorites
クラスのタイプセーフを破ってくるということだ。これを防ぐためには、putFavorite
メソッドにおいて、格納するインスタンスが本当にキーと同じ型であるかを、cast
メソッドを使って確かめればよい。
// Achieving runtime type safety with a dynamic cast
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
これについては、checkedSet, checkedList, checkedMap などでも同じ技術が使われている。これらのラッパークラスは、ジェネリックスとrawタイプが混在したアプリケーションにおいて、クライアントコードから不正な要素の追加をしているところを見つけ出すのに有用である。
2つ目は、Favorites
クラスはnon-reifiableな型(Item28)を利用できない。つまり、String
や、String[]
を格納することはできるが、List<String>
を格納することはできない。これに関しては、満足いく回避策はない。
上述のFavorites
クラスでは型に境界はなく、getFavorite
もputFavorite
もあらゆる型を受け入れていた。もし、受け入れる型に制限を加えたいのであれば、境界のある型パラメータ(Item30)や境界ワイルドカード(Item31)を使える。
アノテーションAPI(Item39)では境界のある型パラメータの拡張的な使い方をしている。例として、実行時にアノテーションを読むメソッドを考える。このメソッドはAnnotatedElement
インターフェースにあり、クラス、メソッド、フィールドなどの要素を代表した、リフレクティブ型(reflective types)に実装される。(リフレクティブ型??)
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
引数であるannotationType
はアノテーションの型を表すが、境界のある型変数が使われている。このメソッドは、引数で受け取った型のアノテーションがあればそれを返し、なければnullを返す。重要なのは、アノテーションを付与された要素は、キーがアノテーション型である、typesafe heterogeneous container であるということである。
仮にClass<?>
という型のオブジェクトを持っていたとして、引数に境界のある型パラメータを求めるようなメソッド、つまり、今回のgetAnnotation
に渡すとする。その場合には、Class<T extends Annotation>
へのキャストをすると思うが、このキャストはコンパイル時にワーニングが出る(Item27)。
このような時には、asSubclass
メソッドを用いる。このメソッドは、自身が引数に取ったClass
オブジェクトのサブクラスであれば、自身のClass
オブジェクトを戻し、そうでなければClassCastException
をスローするというものである。
結果、以下の実装となる。
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}