第5章から Effective Java シリーズ再開。第5章はジェネリックスについて。
今回は「項目23:新たなコードで原型を使用しない」。
ポイント
用語
まず、ジェネリックスに関する用語を定義します。
型パラメータ(type parameter)をもつクラスやインタフェースは、「ジェネリッククラス」や「ジェネリックインタフェース」と呼ばれます。 これらはまとめてジェネリック型(generic type)として知られています。
List クラスを例にとります。
ジェネリック型 List <E> は、「<>」で囲まれた仮型パラメータ E を持ちます。 一方、パラメータ化された型 List<String> は「<>」で囲まれた実型パラメータ String を持ちます。
ジェネリック型に実型パラメータを与えないこともできます。 これを原型(raw type)と呼びます。
それぞれをまとめると以下のようになります。
名前 | 例 |
---|---|
ジェネリック型 | List<E> |
仮型パラメータ | E |
パラメータされた型 | List<String> |
実型パラメータ | String |
原型 | List |
原型
引き続き List クラスを例に取り原型を説明します。
原型を使うリストは以下のように宣言します。
List stamps = new ArrayList();
このリストはその変数名から Stamp オブジェクトを挿入し、Stamp オブジェクトを取り出すことを期待すると思います。
実際には以下のようなコードを書くことができ、コンパイルして実行することができます。 ただし、実行すると ClassCastException が発生してしまいます。
stamps.add(new Coin(...)); Stamp s = (Stamp) stamps.get(0); // => ClassCastException の発生
ジェネリックス型
一方、ジェネリックス型を使ったリストは以下のように宣言します。
// Java 7 から、右辺のブラケット内は省略可能なので省略します。 List<Stamp> stamps = new ArrayList<>();
このリストに対し、Coin オブジェクトを追加しようとするとコンパイルエラーが発生します。
stamps.add(new Coin(...)); // => コンパイルエラーが発生する。
このように、コンパイル時に間違いを発見できるだけでなく、明示的なキャストをコード上から取り除くこともできます。
Stamp s = stamps.get(0); // => キャスト不要
コンパイラがジェネリックス情報を利用して型を解析し、自動的にキャストを挿入しているためです。
ただし、これは「ジェネリクスがサポートされているコンパイラを利用し、一切警告がでていない」という条件下において有効です。
原型を利用しない
上で述べたように、原型はコンパイルすることができ(コンパイル警告はでます)、コンパイルされたコードは実行することができます。 しかし、実行時に ClassCastException が発生する可能性があるため、原型は利用するべきではありません。
では、なぜ原型が許されているのでしょうか。
それは、もともと Java にはジェネリックスが存在しなかったためです。 ジェネリックスは Java 1.5 で初めてサポートされ、移行互換性のために最新の Java でも原型はサポートされています。
逆にいうと、Java 1.4 以前ではすべてが原型のような仕組みで動作していたのです。
List<Object>
List のような原型は利用するべきではありませんが、任意のオブジェクトを挿入できる List<Object> を利用することは何も問題ありません。 前者では型検査が行われない一方、後者では型検査が行われ実行時のエラーを防いでくれます。
たとえば原型を利用した次のようなメソッドがあります。
private static void unsafeAdd(List list, Object o) { list.add(o); }
このメソッドを以下のように利用すると、ClassCastException が発生してプログラムは終了してしまいます。
public static main (String[] args) { List<String> strings = new ArrayList<>(); unsafeAdd(strings, new Integer(42)); Strings = strings.get(0); // => ClassCastException の発生 }
一方、ジェネリックスを用いて unsafeAdd を実装すると以下のようになります。
public static void unsafeAdd(List<Object> list, Object o) { list.add(o); }
この場合、さきほどのコードはコンパイルできなくなります。
public static main (String[] args) { List<String> strings = new ArrayList<>(); unsafeAdd(strings, new Integer(42)); // <- コンパイルエラー:メソッド unsafeAdd は指定された型に適用できません。 Strings = strings.get(0); }
このように、List<Object> を利用すればコンパイル時にエラーを見つけることができるのです。
なお、Java では List<Object> に List<String> を代入することはできません。 このように型パラメータのクラスに継承関係があるにも関わらず(この例では String は Object を継承しています)、ジェネリックス型に継承関係がないことを不変(invariant)と呼びます。
この性質の詳細は次回以降に改めて説明します。
非境界ワイルドカード型
型要素が何であるかを気にしないリストに対して原型を使いたくなるかもしれません。 たとえば、2つのリストを比較し、共通の要素の数を返すメソッドを考えてみます。
static int numElementsInCommon(List a, List b) { int result = 0; for (Object o1 : a) { if (b.contains(o1)) { result++ } } return reuslt; }
例のごとく、このメソッドはコンパイル可能ですが、危険な原型を利用しているため、実行時に ClassCastException が発生する可能性があります。
このような場合には「非境界ワイルドカード型」を利用します。
ジェネリックス型 List
非境界ワイルドカード型を利用するとさきほどのコードは以下のようになります。
static int numElementsInCommon(List<?> a, List<?> b) { int result = 0; for (Object o1 : a) { if (b.contains(o1)) { result++ } } return reuslt; }
原型のリストと非境界ワイルドカード型のリストの差は安全性です。
原型のリストにはどのような要素も挿入することができますが、実行時に ClassCastException が発生する可能性があります。 一方、非境界ワイルドカード型のリストには通常のオブジェクトは挿入できません(null は挿入可能)。
実際に挿入しようとすると、コンパイルエラーが発生しコンパイルできません。
原型を利用するケース
一般的に、原型を利用するべきではありませんが、利用しなければならないケースが2つあります。
- クラスリテラルを使う場合
- List.class は利用できますが、List<String>.class は利用できません
- instanceof 演算子を使う場合
- instanceof List は利用できますが instanceof List<String> は利用できません。
これらは二つとも『ジェネリックスによる型情報は実行時に消されている』という事実に基づいています。
もし、instanceof 演算子を使った場合は非境界ワイルドカード型にキャストして、利用することが推奨されます。
if (o instanceof List) { List<?> list = (List<?>) o; ... }
まとめ
このように、ジェネリックスではコレクションの要素が何であるかをコンパイラに伝え、コンパイラが自動的にキャストを挿入してくれます。
また、コンパイル時に型検査が行われるため、ClassCastException が発生する可能性が大幅に減り、プログラムの安全性を高めることができます。
感想
さすがに最近はもうジェネリックス使わないコード見かけないな。
不変、共変、反変あたりと非境界ワイルドカード型あたりの話は、別の機会に調べてまとめておきたいな。
あと、なんとなく説明を「ですます」調にしてみたら、書きやすかった。