Effective Java 第 2 版:第 5 章 項目 28, 29
ジェネリックスの章、最後の 2 項目。ワイルドカードを使った API と、マップのキーのパラメータ化について。
項目 28: API の柔軟性向上のために境界ワイルドカードを使用する
extends を用いた境界ワイルドカード
スタックを考える。pushAll は以下の API を考える。
public void pushAll(Iterable<E> src);Number は Integer のスーパークラスである。よって、Number でパラメータ化されたスタックに、Integer でパラメータ化されたオブジェクトを格納することは自然に思える。しかし、実際は以下のようなコードはエラーになる。
Stack<Number> stack = new Stack<>();Iterable<Integer> iterable = ...;stack.pushAll(iterable); // エラーここで境界ワイルドカードを使う。この境界ワイルドカードを使って、以下のようにコードを書き換えると、上記のコードは動く。
public void pushAll(Iterable<? extends E> src);? extends E は E のサブクラスであるなんらかの型を表す。こうすることで、Number のサブクラスである Integer, Double などでパラメータ化されているオブジェクトが、引数として渡せるようになる。
super を用いた境界ワイルドカード
スタックに対する格納メソッドである popAll を考える。
public void popAll(Collection<E> dest);このコードだと、さきほどの Number と Integer の話と同様の理由で、以下のコードが動かない。
Stack<Integer> stack = Stack<>();Collection<Number> numbers = ...;stack.popAll(numbers);このとき、境界ワイルドカードを用いて、以下のように書き換えることで、上記のコードが動くようになる。
public void popAll(Collection<? super E> dest);? super E は E のスーパークラスであるなんらかの型を表す。こうすることで、Number のサブクラスである Integer, Double などでパラメータ化されているオブジェクトを、引数のオブジェクトへ格納できるようになる。
PECS
メソッドに T を与えるための引数は、型パラメータを extends とする。逆に、メソッドから T をもらうための引数は、型パラメータを super とする。これを表す略語として PECS (producer-extends, consumer-super) がある。PECS にのっとることで、API が柔軟に使えるようになる。
項目 27 の max の API は、PECS により、以下のように書き換えられる。
public static <T extends Comparable<? super T>> T max(List<? extends T> list);max 自体の引数の型が List<? extends T> であることは、この与える引数から最大値を求める、ということから、PECS により理解できる。
それより少しわかりにくいのは Comparable の型パラメータである。これは super を使っているため、T を与えられるということになる。Comparable<T> は T と比較可能なことを表すインターフェースである。つまり、Comparable<T> は、T を受け取って、compareTo によって、自分自身と比較するオブジェクトということになる。これは、popAll における Collection<T> と役割としては一緒である(Collection は T を受け取って自分自身に格納している)。よって、Comparable<T> は consumer といえ、PECS により Comparable<? super T> と書き換えられる。
その他
- 型パラメータがメソッド宣言中に一度しか現れないなら
?で置き換える- 型パラメータが
?のコンテナーには(null意外の)要素を入れられないので、その対応は必要
- 型パラメータが
項目 29: 型安全な異種コンテナーを検討する
ジェネリックな Class 型をキーとすることで、複数の型を扱うマップを作れる。
Map<Class<?>, Object> map = new HashMap<Class<?>, Object>>();マップのキーにワイルドカード ? が用いられている。これによって、異なる型のクラスリテラルをキーとして値を格納、取り出しできる。クラスリテラルとは、String.class や Integer.class などで得られる Class<T> 型オブジェクトのことである。
また、値に Object 型が用いられている。こちらは型パラメーターで表せないので、すべてのクラスの先祖である Object にすることで、どのような型のオブジェクトも入れられるようにしている。
このコンテナーに対する API は以下のように実装できる。
public <T> void myPut(Class<T> type, T instance) { if (type == null) { throw new NullPointerException("Type is null"); } map.put(type, type.cast(instance));}public <T> T myGet(Class<T> type) { return type.cast(map.get(type));}上記のメソッドで使われている Class.cast は、引数にとった値が Class の表す型と一致するなら、Object から Class の表す型のオブジェクトへキャストする。そうでなければ、ClassCastException を投げる。
実際の使い方は以下の通り。
myPut(Integer.class, 1); // キー Class<Integer>, 値 int (Integer)myPut(String.class, "test"); // キー Class<String>, 値 Stringint i = myGet(Integer.class);String s = myGet(String.class);制約
- 原型をキーにすると型安全性が崩れる
- コンパイル時に無検査警告が出る
- 具象化不可能型(
List<String>など)をキーに使えないList<String>.classは文法エラーList<Integer>とList<String>は同じClassオブジェクトを持つので、今回のケースではうまくいかない