3️⃣

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

2023/11/07に公開

はじめに

前回は共変性について扱いました。残りは反変性のみとなっていますので、そこをメインとして扱いたいと思います。

下限境界ワイルドカード

上限境界ワイルドカードは非常に便利な機能ではあるのですが、使っている際にnullのみを引数に渡すことができるというのはどうしても不便です。この制約は、上限境界ワイルドカードではデータ型の継承関係上の上限のみを定めることが原因となっていました。言い換えると、引数に渡すことのできるデータ型の継承関係上の下限が定まっていないことが原因だと言える訳ですね。javaにはその下限を定めるためのワイルドカードが存在しています。それを 下限境界ワイルドカード と呼びます。「super」というキーワードを用いて下限を設定するのですが、ここも具体的なコードを見た方がわかりやすいかと思いますので下記コードを参照して下さい。

Sample.java
class Sample<T> {
  private T value;
  T getValue() {
    return this.value;
  }
  void setValue(T value) {
    if(value == null) throw new IllegalArgumentException();
    this.value = value;
  }
}
Main.java
class Main {
  public static void main(String... args) {
    Sample<? super List<String>> sample;
    sample = new Sample<Collection<String>>();
    sample.setValue(new ArrayList<String>()); // 問題なし
  }
}

下限境界ワイルドカードを利用すると、上限境界ワイルドカードとは逆にメソッドの引数に指定されるデータ型が下限として指定されたデータ型のスーパータイプであることが確定しています。Javaにおいては、スーパータイプであればサブタイプのデータを扱えることになっていました。ジェネリクスに指定されるデータ型が、継承関係上の下限が決まっているという条件のもとで不定であったとしても、下限そのものか、その下限のさらにサブタイプであれば実際に型パラメータに指定されたデータ型のサブタイプとなるはずですので、実引数として渡すことが可能となります。よって、コード中のsample.setValue(new ArrayList<String>());という記述が問題なく実行できることになります。

少し複雑ですので詳しく見てみましょう。まず、ローカル変数「sample」のデータ型は下限境界ワイルドカードを利用して、List型[1]をサブタイプとして持つ任意のデータ型を扱うSample型となっています。そのため、この変数にはList型のスーパータイプであるCollection型をジェネリクスに指定したSample型インスタンスへの参照を代入することが認められます。その後setValueメソッドの実引数としてArrayListのインスタンスへの参照を渡しているのですが、今回扱っているSample型インスタンスは型パラメータにArrayListのスーパータイプであるCollectionを指定していますので、問題なく実行ができるということになります。

下限境界ワイルドカードの欠点

下限境界ワイルドカードには上限境界ワイルドカード同様欠点があります。その欠点は上限境界ワイルドカードでは指定できていたメンバメソッドの戻り値が全てObject型になってしまうと言うことです。戻り値のデータ型の継承関係上の上限が定まっていないと言うことは、その値を受け取る変数のデータ型を指定することはできません。そのため、全ての参照型データのスーパータイプにあたるObject型が戻り値として採用されていると言うことです。コード例を以下に提示しておきます。

Main.java
class Main {
  public static void main(String... args) {
    Sample<? super List<String>> sample;
    sample = new Sample<Collection<String>>();
    Object list1 = sample.getValue(); // 問題なし
    List<String> list2 = sample.getValue(); // コンパイルエラー
  }
}

反変性

下限境界ワイルドカードのように、あるデータ型のスーパータイプであればデータ型の差分を認めるという性質を 「反変性(Contravariance)」 と言います。Javaにおいてはこの反変性の実現に主に下限境界ワイルドカードを利用しますので、なかなか見かける機会は少ないかと思います。使うタイミングとしてはメソッドに対する入力を可変にしたい場合になります。共変戻り値などで出力を可変にした共変性とは対比された使い方となる訳です。

非境界ワイルドカード

ここまで境界を持つワイルドカードを扱ってきましたが、最後に境界を有さないワイルドカードに少しだけ触れておきます。「?」のみを指定することで利用できるのですが、前回述べたようにあまり使われることはありません。なぜでしょう。境界を持つワイルドカードを理解した皆さんであれば、予想できるかもしれません。少し考えてみて下さい。

今まで扱ってきた境界を持つワイルドカードにはそれぞれ欠点があったことを覚えているでしょうか。その欠点はそれぞれの境界が存在しない側に発生していました。上限境界ワイルドカードであれば下限が不定であることに、下限境界ワイルドカードであれば上限が不定であることによって問題が起きます。では、その境界をどちらにも持たない非境界ワイルドカードはどうなるでしょうか。もちろん両境界ワイルドカードに発生する問題が両方発生する訳です。つまり、型パラメータを引数定義に利用したメンバメソッドの実引数にはnullしか渡せず、型パラメータを利用した戻り値のデータ型は全てObject型の変数でしか受け取ることができないのです。この理由から非境界ワイルドカードはあまり利用されることがありません。

まとめ

以上でJavaにおける変性は概観できたことになるかと思います。何か足りていない部分などありましたら、コメントで教えていただけると非常に助かります。自分でもだんだん何を書いているのかわからなくなってきまして、たぶん、いや絶対に漏れがあると思います。最初に自分が書こうと思っていたものからもおそらく漏れが出ていることでしょう。穴が見つかり次第修正していく予定です。

脚注
  1. ジェネリクスを省略していますが、前々回で扱った通りジェネリクスは非変性であるので、今回はStringで全て固定されています。 ↩︎

Discussion