読者です 読者をやめる 読者になる 読者になる

【Java】ジェネリックス型の不変、共変、反変とは何か

今回はジェネリックスの不変、共変、反変について書いてみた。

本当は Effective Java 「項目25:配列よりリストを使う」の予定だったんだけど、不変、共変、反変あたりの話がでてきて、 ここらへんは以前からまとめておきたかったし、ちょうどよいと思って記事にした。

不変、共変、反変

不変、共変、反変とはそれぞれ、ジェネリクスの性質を指す用語です。 話を具体的にするため、例として List<E> と、Object、String を使って説明します。

Java の Object、String には以下のような関係があります。

  • Object は String のスーパータイプである

この時、Object と String に対してパラメータ化された型である List<Object> と List<String> の関係性はどうなるでしょうか?

可能性として、以下のような組み合わせを考えることができるはずです。

  • List<Object> と List<String> に全く関係性がないケース
  • List<Object> は List<String> のスーパータイプであるケース
  • List<String> は List<Object> のスーパータイプであるケース(この例は少し直感に反しますが、組み合わせの一つとしておかしいものではないはずです)

この考えられる関係をそれぞれ、上から順に「不変」、「共変」、「反変」と呼びます。

すなわち、Object が String のスーパータイプであるとき、

  • 不変(invariant):List<Object> と List<String> には関係性がない
  • 共変(covariant):List<Object> は List<String> のスーパータイプ
  • 反変(contravariant):List<String> は List<Object> のスーパータイプ

と定義されているのです。

図にまとめると、以下のようになります。

f:id:hjm333:20160206231832p:plain

Javaジェネリックス型

不変、共変、反変の定義について説明しましたが、Java においてジェネリックス型は「不変」と決められています。 そのため、「Object は String のスーパータイプ」という関係はありますが、List<Object> と List<String> にはなんの関係性もありません。

結果的に、以下のようなコードをコンパイルすることはできないのです。

List<Object> objects = new ArrayList<String>(); 
// => コンパイルエラー
// Java のジェネリックス型を共変に変更するならばコンパイル可能。
List<String> strings = new ArrayList<Object>();
// => コンパイルエラー
// Java のジェネリックス型を反変に変更するならばコンパイル可能。

共変・反変が許されない理由

では、なぜ共変、反変が許されないのでしょうか?

それは共変、反変を許すと「コンパイルできるのに実行時に型エラーが発生する」という事態を生んでしまうためです。

ジェネリックス型の目的はコンパイル時に型チェックを行い、実行時に ClassCastException を発生させないことです。 共変や反変を許してしまうとこの目的を達成することができません。

実際にコードを書いてみて、「もし共変・反変を許したらどうなるか」を見てみたいと思います。

何らかの値を保持する Holder<T> というジェネリックス型を考えてみます。

class Holder<T> {
    private T mValue;
    
    public Holder(T value) {
        this.mValue = value;
    }
    
    public void setValue(T value) {
        this.mValue = value;
    }
    
    public T getValue() {
        return this.mValue;
    } 
}

まずジェネリックス型を共変にすると仮定し、Holder.getValue と Holder.setValue を利用してみます。 共変だと仮定すると Holder<Object> に Holder<String> を代入することが可能です。

void covariantGet() {
   Holder<Object> objectHolder = new Holder<String>("value");
   // => 共変だと仮定するのでコンパイルエラーは起きない。
   Object obj = objectHolder.getValue();
   // => String インスタンスの "value" を Object 型の obj に代入するので問題なし
}

void covariantSet() {
   Holder<Object> objectHolder = new Holder<String>("");
   // => 共変だと仮定するのでコンパイルエラーは起きない。
   objectHolder.setValue(new Object());
   // => 実行時エラー
   // Holder<Object> の setValue は Object 型の引数を受け付けるのでコンパイルエラーは発生しない。
   // しかし、setValue 内で String 型 this.mValue に Object インスタンスは代入できず実行時エラー
}

このように共変を許可すると、Holder.setValue で実行時エラーが発生してしまいます。

一方、ジェネリックス型を反変にすると仮定し、Holder.getValue と Holder.setValue を利用してみます。 反変だと仮定すると Holder<String> に Holder<Object> を代入することが可能です。

void contravariantGet() {
   Holder<String> stringHolder = new Holder<Object>(new Object());
   // => 反変だと仮定するのでコンパイルエラーは起きない。
   String string = stringHolder.getValue();
   // => 実行時エラー
   // Holder<String>.getValue の返り値は String なのでコンパイルエラーは発生しない。
   // ただし、stringHolder の this.mValue は Object インスタンスであり、String 型には変換できず実行時エラー
}

void contravariantSet() {
   Holder<String> stringHolder = new Holder<Object>(new Object());
   // => 反変だと仮定するのでコンパイルエラーは起きない。
   stringHolder.setValue(new String());
   // => Object 型の this.mValue に String インスタンスを代入するので問題なし
}

この場合、getValue の方で実行時エラーが発生してしまうので、反変にすることはできません。

このように、ジェネリックス型を共変にした場合は T を引数にしている setValue(T value) で、ジェネリックス型を反変にした場合は T を戻り値としている T getValue() で実行時エラーが発生してしまいます。

共変と反変による制限

上で述べたように、共変なジェネリクス型はメソッドの引数に型パラメータ E を利用できず、反変なジェネリクス型は戻り値に型パラメータ E を利用できません。

  • 共変:型パラメータ E を引数に使えなくなる
  • 反変:型パラメータ E を戻り値に使えなくなる
  • 不変:引数にも戻り値にも型パラメータ E を使える

ここで「型パラメータ E を引数に使わなければ、共変にできるんじゃないの?」という疑問がわくと思います。 残念ながら Java ではすべてのジェネリックス型は不変に固定されていますが、実際、プログラミング言語によっては共変・反変をジェネリックス型ごとに指定することができます。

たとえば ScalaC# の場合、それが可能になっています。

それでも、上記の制限は変わりません。 共変を指定した場合には型パラメータ E を引数としては使えず、反変を指定した場合には型パラメータ E を戻り値には使えません。

もし、そのようなコードを書くと、ScalaC# でもコンパイルエラーとなります。

さいごに

ここまで「Javaジェネリックス型は不変である」と説明してきましたが、実は Java では配列に関しては共変です。 そのため、Object[]String[] のスーパータイプとして扱うことができます。

この話は次回の Effective Java Item 25 で説明します。

また、Java では共変・不変を指定できませんが、似たような性質を持つ「非境界ワイルドカード型」を利用することができます。 この方法については次回以降の Effective Java シリーズでとりあげます。

感想

本当は Scala の不変・共変の指定例とか実際のコンパイルエラーとかも説明したかったが、それはまた今度。

ここで説明している共変とか不変とか、スーパータイプなどの言葉はかなりカジュアルに使っていて、厳密な理論に基づいてはいないので、そういうのが知りたい方はぜひ『型システム入門』を読んでください。

型システム入門 −プログラミング言語と型の理論−

型システム入門 −プログラミング言語と型の理論−

  • 作者: Benjamin C. Pierce,住井英二郎,遠藤侑介,酒井政裕,今井敬吾,黒木裕介,今井宜洋,才川隆文,今井健男
  • 出版社/メーカー: オーム社
  • 発売日: 2013/03/26
  • メディア: 単行本(ソフトカバー)
  • クリック: 68回
  • この商品を含むブログ (11件) を見る