Javaのジェネリクスの型安全について自分なりにまとめてみた
Effective Java 第3版を読んでみたところ、自分がジェネリックスの型安全について理解できていないことがわかったのでまとめてみました。
- はじめに
- Javaの型変換
- ジェネリクス型
- ジェネリクス型の継承関係
- 不変性(invariant)
- 共変性(covariant)
- 反変性(contravariant)
- ジェネリクスの不変・共変・反変のまとめ
- 参考
はじめに
Animal クラスを継承した Cat クラスと Human クラスについて考える。
public class Animal { } public class Cat extends Animal { } public class Human extends Animal { }
記事の中では主に上記のクラスを用いて解説を行う。
Javaの型変換
ジェネリクスの型安全について考えていると頭が混乱するので、ひとまず頭を整理するため Java の型変換について復習する。
アップキャスト
アップキャストとはスーパークラス(親クラス)からサブクラス(子クラス)へ変換のことである。
子クラスは親クラスを継承しているため、親クラスのフィールドや関数を全て持っている。そのため、親クラスに、子クラスのインスタンスを代入することができる。
Cat cat = new Cat();
Animal animal = cat;
ダウンキャスト
ダウンキャストとはサブクラスからスーパークラスへの変換のことである。
親クラスは子クラスが持つ独自の関数やフィールドを持っていないため、親クラスのインスタンスを子クラスに代入しようとするとコンパイルエラーになる。
Animal animal = new Animal(); Cat cat = (Cat) animal; // java.lang.ClassCastException: class Animal cannot be cast to class Cat
ダウンキャストを行うときは親クラスの実体が子クラスでなければならない。
つまり、以下のように親クラスを子クラスでインスタンス化する必要がある
Animal animal = new Cat();
Cat cat = (Cat) animal;
ジェネリクス型
List<Animal>
の<Animal>
の部分。
List には様々なオブジェクトを入れることができるが、ジェネリクスを使うと代入できる型を限定することができる。
コンパイル時に型を検査してエラーを出してくれるようになり、コードを見たときに何の型を扱っているのか明確になるのでプログラマが幸せになれる。
以下のように引数の型を T
とすることで、インスタンス生成時に型を指定することができる。
public class Generic<T> { private T value; public void setValue(T val) { value = val; } public T getValue() { return value; } } Generic generic = new Generic<Integer>(); // インスタンス作成時に型を指定
非境界ワイルドカード型
要素の型が何であろうと構わないような場合は 非境界ワイルドカード型 を使う。
非境界ワイルドカード型は型パラメータにクエスチョン記号を付ける。例:List<?>
非境界ワイルドカード型は型安全である。原型のコレクションにはどのような要素も挿入できるが、非境界ワイルドカード型のコレクションには、(null以外の) 要素が挿入できない。また、非境界ワイルドカード型の要素はどのような型にでもなりえるため、取得した要素型はもっとも汎用的な型である Object となる。
Generic<?> generic = new Generic<Integer>(); generic.setValue(null); // nullのみ代入できる Object value = generic.getValue(); // どのような型が返ってくるか保証できないので Object が返る
ジェネリクス型の継承関係
型パラメータ Animal を持つ List<Animal> にインスタンスを格納してみる。
Cat も Human も Animal を継承しているため、List<Animal> にインスタンスを代入することができる。
List<Animal> animals = new ArrayList<Animal>(); animals.add(new Animal()); animals.add(new Cat()); animals.add(new Human());
次に List<Animal> と List<Cat> について考える。
直感的には List
List<Animal> animals = new ArrayList<Cat>(); // error: incompatible types: ArrayList<Cat> cannot be converted to List<Animal>
同様に以下もコンパイルエラーとなる。
List<Cat> cats = new ArrayList<Animal>(); // error: incompatible types: ArrayList<Animal> cannot be converted to List<Cat>
コンパイルエラーとなる理由は、Java では型パラメータに指定したクラスに継承関係があったとしても別の型として扱われるからである。
これを不変(invariant)であると言う。つまり、ジェネリクスは不変である。
また、ジェネリクスが不変ではないと仮定する。
List<Animal> animals = new ArrayList<Cat>();
が成り立つ場合を共変(covariant)、
List<Cat> cats = new ArrayList<Animal>();
が成り立つ場合を反変(contravariant)という。
不変性(invariant)
List<Animal> と List<Cat> に関係性がないとき、ジェネリクスは不変である。 ジェネリクスに不変性しか持たせられない場合、ジェネリクスを引数とする汎用的なメソッドを定義したい場合に困ることになる。
public void tmpAllList(List<Animal> animals) { .... } List<Cat> cats = new ArrayList<Cat>(); cats.add(new Cat()); tmpAllList(cats); // List<Animal>しか受け付けないのでコンパイルエラー
様々なジェネリクスを引数にとる汎用的なメソッドを定義したいとき、それぞれのジェネリクスを引数とする同じ名前のメソッドを量産することになる
共変性(covariant)
ジェネリクスが共変であると仮定する
List<Cat> が List<Animal> のサブタイプであるとき、ジェネリクスは共変である。
ジェネリクスは不変だが、共変だったと仮定すると以下のように扱うことができる。
List<Animal> animals = new ArrayList<Cat>();
この定義が有効である場合について、まず、List<Animal>
から要素を取得する場合について考えてみる。
List<Animal>
の要素の返り値はAnimal
、Cat
、Human
のいずれかのインスタンスになる。これらは Animal型 にキャストすることが可能なため問題はない。
List<Cat> cats = new ArrayList<Cat>(); cats.add(new Cat()); List<Animal> animals1 = cats; List<Human> humans = new ArrayList<Human>(); humans.add(new Human()); List<Animal> animals2 = human; Animal animal1 = animals1.get(0) // Catのインスタンスが取り出せる Animal animal2 = animals2.get(0) // Humanのインスタンスが取り出せる
次に、List<Animal>
に要素を追加する場合について考えてみる。
List<Animal>
に追加できる要素のインスタンスはAnimal
、Cat
、Human
である。しかし、List<Animal>
の実体が List<Cat>
である場合、Cat のリストに Human のインスタンスが存在することになるので Java の型安全を破棄することになる。
List<Cat> cats = new ArrayList<Cat>(); List<Animal> animals = cats; animals.add(new Human()); // ランタイムエラー Catのリストに Human が入ることになる
このような問題があるため、Javaのジェネリクス型は共変になっていない。
共変は要素を取り出すときには問題はないが、要素を追加するときに問題がある。
ジェネリクスに共変性を持たせる
上限境界ワイルドカード型 を使うとジェネリクスに共変性を持たせることができるようになる。例: List<? extends Animal>
共変は互換の無いインスタンスを格納できてしまうという問題があったが、上限境界ワイルドカード型の場合は要素を追加しようとするとコンパイルエラーとなる。これにより Java の型安全が守られる。
List<? extends Animal> animals = new ArrayList<Animal>(); animals.add(new Animal()); // コンパイルエラー animals.add(new Cat()); // コンパイルエラー animals.add(new Human()); // コンパイルエラー animals.add(null); // nullのみ代入可
配列は共変
Java の配列は共変であるため、Object の配列に Long の配列を代入してもコンパイルが通る。
Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; // ArrayStoreException
反変性(contravariant)
ジェネリクスが反変であると仮定する
List<Cat> が List<Animal> のスーパータイプであるとき、ジェネリクスは反変である。
ジェネリクスは不変だが、反変だったと仮定すると以下のように扱うことができる。
List<Cat> cats = new ArrayList<Animal>();
この定義が有効である場合、List<Cat>
の実体はList<Animal>
であるため、要素としてAnimal
、Cat
のインスタンスを追加できる。
cats.add(new Cat()); // 実体がAnimalのリストに Cat を代入しても問題はない
次にList<Animal>
の要素を取得する場合について考えてみる。
List<Animal>
に追加できる要素のインスタンスはAnimal
、Cat
、Human
である。そのため、List<Animal>
にHuman
のインスタンスを入れることができる。List<Cat>
のサブクラスはList<Animal>
(反変)であるため、List<Animal>
をList<Cat>
にキャストできる。しかし、要素としてはHuman
のインスタンスを持つのでエラーとなる。
List<Animal> animals = new ArrayList<Animal>(); animals.add(new Human()); List<Cat> cats = animals; Cat cat = animals.get(0); // ランタイムエラー Humanのインスタンスが返る
このような問題があるため、Javaのジェネリクス型は反変になっていない。
反変は値を追加するときには問題はないが、値を取得するときに問題がある。
ジェネリクスに反変性を持たせる
下限境界ワイルドカード型 を使うとジェネリクスに反変性を持たせることができるようになる。例: List<? super Animal>
反変は互換の無いインスタンスを取得できてしまうという問題があったが、上限境界ワイルドカード型の場合は要素を取得しようとするとコンパイルエラーとなる。これにより Java の型安全が守られる。
List<? super Animal> animals = new ArrayList<Animal>(); animals.add(new Object()); animals.add(new Animal()); animals.add(new Cat()); animals.add(new Human()); Object object = animals.get(0); // OK Animal animal = animals.get(1); // コンパイルエラー Cat cat = animals.get(2); // コンパイルエラー Human human = animals.get(3); // コンパイルエラー
ジェネリクスの不変・共変・反変のまとめ
- 不変(invariant): List<Animal> と List<Cat> には関係性がない
- 共変(covariant): List<Cat> は List<Animal> のサブタイプ
- 要素を追加するときに型安全を破棄してしまう。
- ジェネリクスに共変性を持たせるときは 上限境界ワイルドカード型(List<? extends T>) を使う。
- 反変(contravariant): List<Cat> は List<Animal> のスーパータイプ
- 要素を取得するときに互換性のない型が取得できてしまう。
- ジェネリクスに反変性を持たせるときは 下限境界ワイルドカード型(List<? super T>) を使う。