1. ホーム
  2. java

[解決済み] 共分散、不変化、共分散をわかりやすく解説?

2022-05-24 15:51:06

質問

今日、私はJavaの共分散、共分散(および不変量)についてのいくつかの記事を読みました。私は英語とドイツ語の Wikipedia の記事、および IBM からのいくつかの他のブログ投稿と記事を読みました。

しかし、これらが正確に何についてなのか、私はまだ少し混乱しています。ある人は型とサブタイプの関係について、ある人は型変換について、ある人はメソッドがオーバーライドされるかオーバーロードされるかを決定するために使用されると言います。

そこで、CovarianceとContravariance(とInvariance)が何なのか、初心者にわかりやすく説明するものを探しています。簡単な例のためのプラスポイント。

どのように解決するのですか?

<ブロッククオート

型とサブタイプの関係であるとか、型変換であるとか、メソッドが上書きされるかオーバーロードされるかの判断に使われるとか、いろいろ言われているようです。

上記のすべてです。

要するに、これらの用語はサブタイプ関係が型変換によってどのように影響されるかを説明しています。つまり、もし AB は型である。 f は型変換、≦はサブタイプ関係(すなわち A ≤ B が意味するのは A のサブタイプである。 B のサブタイプである)、次のようになります。

  • f が共変数である場合 A ≤ B を意味する。 f(A) ≤ f(B)
  • f が逆変量である場合 A ≤ B を意味する。 f(B) ≤ f(A)
  • f は、上記のどちらも成立しない場合、不変である。

例を考えてみましょう。以下のようにします。 f(A) = List<A> ここで List が宣言されています。

class List<T> { ... } 

f は共変、共変、不変のどれでしょうか?共変とは List<String> のサブタイプであることを意味します。 List<Object> のサブタイプであり List<Object> のサブタイプである。 List<String> のサブタイプであり、どちらも他方のサブタイプではないことが不変であること、すなわち List<String>List<Object> は変換不可能な型です。Javaでは、後者は真であり、我々は(やや非公式に)次のように言う。 ジェネリックス は不変であると言います。

もう一つの例です。ここで f(A) = A[] . は f は共変数か、共変数か、不変量か?つまり、String[]はObject[]のサブタイプなのか、Object[]はString[]のサブタイプなのか、それともどちらもサブタイプでないのか?(答え:Javaでは、配列は共変数です。)

これでもまだ抽象的でしたね。もっと具体的にするために、Javaではどのような操作がサブタイプ関係で定義されているのかを見てみましょう。最も簡単な例は代入です。この文は

x = y;

は、以下の場合にのみコンパイルされます。 typeof(y) ≤ typeof(x) . つまり,今,私たちは

ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();

はJavaではコンパイルされませんが

Object[] objects = new String[1];

になります。

サブタイプの関係が問題となるもう一つの例は、メソッド呼び出し式です。

result = method(a);

非公式に言えば、この文は a の値をメソッドの最初のパラメータに代入し、 メソッドの本体を実行し、メソッドの戻り値を result . 最後の例での単純な代入と同様に、"右手側" は"左手側" のサブタイプでなければなりません。 typeof(a) ≤ typeof(parameter(method))returntype(method) ≤ typeof(result) . つまり、method がによって宣言されている場合。

Number[] method(ArrayList<Number> list) { ... }

はいずれもコンパイルされません。

Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());

しかし

Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());

になります。

サブタイトリングが問題となるもう一つの例はオーバーライドです。考えてみてください。

Super sup = new Sub();
Number n = sup.method(1);

ここで

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n);
}

非公式に、ランタイムはこれを次のように書き換えます。

class Super {
    Number method(Number n) {
        if (this instanceof Sub) {
            return ((Sub) this).method(n);  // *
        } else {
            ... 
        }
    }
}

マークされた行をコンパイルするためには、オーバーライドするメソッドのメソッドパラメータはオーバーライドするメソッドのメソッドパラメータのスーパータイプでなければならず、リターンタイプはオーバーライドするメソッドのもののサブタイプである必要があります。形式的に言えば f(A) = parametertype(method asdeclaredin(A)) は少なくとも反変数でなければならず、もし f(A) = returntype(method asdeclaredin(A)) は少なくとも共変でなければなりません。

上記の "at least" に注意してください。これらは、合理的な静的型安全オブジェクト指向プログラミング言語が強制する最低限の要件ですが、プログラミング言語はより厳しくすることを選択することができます。Java 1.4 の場合、メソッドをオーバーライドするとき、パラメータの型とメソッドの戻り値の型は (型の消去を除いて) 同一でなければなりません。 parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B)) をオーバーライドする場合。Java 1.5以降では、オーバーライド時に共変の戻り値の型が許可されます。すなわち、以下のようにするとJava 1.5ではコンパイルされますが、Java 1.4ではコンパイルされません。

class Collection {
    Iterator iterator() { ... }
}

class List extends Collection {
    @Override 
    ListIterator iterator() { ... }
}

私はすべてをカバーしたことを願っています - というより、表面を掻いただけです。それでも、抽象的ではありますが、型分散の重要な概念を理解する助けになればと思います。