Javaのジェネリクスの型安全について自分なりにまとめてみた

Effective Java 第3版を読んでみたところ、自分がジェネリックスの型安全について理解できていないことがわかったのでまとめてみました。


はじめに

Animal クラスを継承した Cat クラスと Human クラスについて考える。

public class Animal { }
public class Cat extends Animal { }
public class Human extends Animal { }

f:id:dev-moyashi:20191227233419p:plain:w200

記事の中では主に上記のクラスを用いて解説を行う。


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<Cat> のインスタンスを代入できそうだが、これはコンパイルエラーとなる。

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>の要素の返り値はAnimalCatHumanのいずれかのインスタンスになる。これらは 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>に追加できる要素のインスタンスはAnimalCatHumanである。しかし、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>であるため、要素としてAnimalCatのインスタンスを追加できる。

cats.add(new Cat()); // 実体がAnimalのリストに Cat を代入しても問題はない

次にList<Animal>の要素を取得する場合について考えてみる。

List<Animal>に追加できる要素のインスタンスはAnimalCatHumanである。そのため、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>) を使う。


参考