1️⃣

Javaにおける共変性・反変性・非変性について①

2023/11/07に公開

はじめに

今回からJavaの学習を進めていくと出くわす共変性や反変性、非変性について考えてみたいと思います。本当は一枚の記事の予定だったのですが、書いているうちに少しずつ長くなってしまいまして数回にわたって投稿することになるかと思います。
未経験からJavaを始める方はおそらくJava SiverやJava Goldの試験対策が最初に見る機会なのかなと勝手に想像したりしていますが実際はどうなんでしょうか。そういった試験対策で詰まった方たちの助けになったりすると嬉しいと思っています。

◯変性

さていきなり本題なのですが、Javaなどのプログラミング言語の文脈内で現れる「◯変性」について一緒に考えていきましょう。この○にはタイトルの通り「共」、「反」、「非」が当てはまる訳ですが、実際当てはめてみるとそれぞれ見たことがあるようないような。。。そもそもこの単語はどのような機会に見たり使ったりしたんでしょうか。少し思い出してみてください。

ジェネリクス

これらの単語は大体の場合ジェネリクスという仕組みと一緒に見かけるのかなと思います。いやいやオーバーライドと一緒に出てきたよという方、ちょっとだけ待っててください。後々必ず触れますので。
では、以下のコードでジェネエリクスの確認をしましょう。コード内の<String>にあたるものがジェネリクスです。

ジェネリクスの確認
List<String> list;

おそらく皆さん見たことがあると思います。型パラメータを指定するために利用されていて、ジェネリクスを従えるクラスがフィールドやメンバメソッドの戻り値及び引数としてどんな型のデータを扱うものかを、 実際にそのクラスを利用するタイミングで決定するため に使われています。ジェネリクスは、フィールドやメンバメソッドの戻り値及び引数のデータ型を変数にしてしまって多様性を実現し、コードの再利用性や柔軟性を高めようということを目的に導入された仕組みだったのでした。ジェネリクスを用いて定義されたクラスでは、定義の仕方にもよりますが、フィールドやメンバメソッドの戻り値及び引数のデータ型が定義時点では定まっていません。以下のコードを見てみてください。

Sample.java
class Sample<T> {
  private final T value;
  Sample(T value) {
    if(value == null) throw new IllegalArgumentException();
    this.value = value;
  }
  T getValue() {
    return this.value;
  }
}

このコードではSampleというクラスが定義されていますが、クラス名の隣にある<T>という部分に注目してください。これがジェネリクスです。実際にSampleクラスを利用する場合には、この部分に参照型データ型を記述(代入)してクラス定義内で「T」としている、つまり不定となっているデータ型を決定します。以下にコード例を示しておきます。

型パラメータの指定
Sample<String> sample = new Sample<String>("引数に渡すデータ型はStringです。");

補足ですが、変数宣言と同時に変数の初期化を行う際には、new Sample<String>("引数に渡すデータ型はStringです。")の型パラメータ指定をnew Sample<>("引数に渡すデータ型はStringです。")のように省略することができます。また、型パラメータを指定せずに変数宣言をすることも可能ですが、その場合には型パラメータとして「Object」を指定したという扱いとなるため注意が必要です。

◯変性とジェネリクス

では、なんでジェネリクスと◯変性は一緒に見かけることが多いんでしょうか。その理由を考えていきましょう。まずは以下のコードを読んで、その挙動を想像してみてください。

Parent.java
class Parent { }
Child.java
class Child extends Parent { }
Main.java
import java.util.List;
import java.util.ArrayList;

class Main {
  public static void main(String... args) {
    List<Child> children = new ArrayList<>();
    List<Parent> parents = children;
  }
}

想像できましたでしょうか。では正解です。このコード実際書いてコンパイルしてみるとわかるんですが、List<Parent> parents = children;の行でコンパイルエラーとなってしまします。

Main.java
import java.util.List;
import java.util.ArrayList;

class Main {
  public static void main(String... args) {
    List<Child> children = new ArrayList<>();
    List<Parent> parents = children; // コンパイルエラー
  }
}

なぜそうなるのかといえば、変数のクラスと同じクラスのインスタンスであっても、それぞれのジェネリクスに指定されたデータ型が異なる場合、そのインスタンスへの参照を対象の変数に代入することが認められていないことが原因となります。この決まり事にはジェネリクスに指定されたデータ型の親子関係なども考慮されません。

あれ、Javaではポリモーフィズムが使えるから親クラス型で子クラスインスタンスへの参照を扱えるんじゃないの?と思った方がいるかと思います。私も最初そう思いました。ポリモーフィズムの原則からすればList<Parent> parents = children;の行は正しく動くようにしか見えません。実際固定長配列では同じようなことをしてもコンパイルエラーにはなりませんからね。

Listではなぜこの原則が認められないのでしょう。これを知るためには、以下のような状況を考えてみるといいかもしれません。

固定長配列Ver.
Integer[] intArray = {1, 2, 3};
Number[] numArray = intArray;
numArray[0] = Double.valueOf(1.2);

このコードはコンパイルできるでしょうか。また、問題なく実行できるでしょうか。少し考えてみてください。

答えは 「コンパイルはできるが、実行中に例外が発生する。」 となります。詳しくみてみましょう。まずコンパイルの可否ですが、Javaにおける固定長配列に関しては問題なくポリモーフィズムを適用できるため、Integer型の配列への参照をNumber型配列型の変数に代入することが可能です。そして、Number型配列の各要素は当然Number型ですから、Double型インスタンスの参照を扱うことが出来ます。以上の理由から問題なくコンパイルはできる訳です。ですが、このコードを実行してみると「ArrayStoreException」という例外が発生してしまします。これは、実際Double型インスタンスの参照を代入してみようとしたところ、Integer型データ配列では互換性がなく扱うことが出来ませんでしたということを示しています。Integer型とDouble型には親子関係はありませんから、配列側が代入されたデータをうまく扱いきれなかった訳ですね。

非変性

このような不具合を発生させないために、Listなどのジェネリクスを利用したデータ型では同じ型パラメータが指定されていない限り、同じデータ型インスタンスへの参照であっても代入が認められないように設計されました。[1]このような設計、性質を 「非変性(Invariance)」 と言います。扱うデータ型として一切の差分を認めない、つまり全く同じデータ型でないと扱うことを認めませんよという性質です。非変性の世界では以下のようなコードは認められないことになります。

非変性の世界
Object obj = new String("text"); // 非変性の世界ではコンパイルエラー

非変性の世界では、変数に代入するデータのデータ型は厳密に変数のデータ型と一致している必要がありますので、上記のコードがコンパイルされることはありません。

しかしながら普通にJavaを書いている分にはこのコードはコンパイルも実行もできます。ポリモーフィズムが効くはずですからね。このことからポリモーフィズムは非変性の世界とは異なった世界の仕組みと言えそうです。

まとめ

今回は◯変性のうち、非変性について紹介しました。Javaにおいて非変性とは簡単にいってしまえば、「変数のデータ型とそれに代入されるデータのデータ型が一致する」 という性質のことを指しています。この性質を採用した設計になっているために、あるList型の変数に対して異なるデータ型を扱うListへの参照を代入することが認められないのでした。

次回はポリモーフィズムの確認から共変性と、できれば反変性まで扱っていこうかなと思います。

脚注
  1. 固定長配列に関しては、それ以前のバージョンとの互換性への考慮から、そのままの仕様が残っています。 ↩︎

Discussion