共変性と反変性とはなんぞや?
共変性と反変性について、うろ覚えだったので改めて再確認し、忘れないようにここに書いておきます。
「共変性」と「反変性」言葉としてはあまり日常的にはでてきませんが、無意識のうちにつかっています。
理解できているようでなかなか理解ができていない、説明するのも難しいです。
(コンピュータプログラミングの型システムでの)共変性と反変性(きょうへんせいとはんぺんせい、covariance and contravariance)とは、データコンテナのサブタイプ関係が、そのデータ要素のサブタイプ関係に連動して定義されるという概念を指す。また、関数の型のサブタイプ関係での、引数型と返り値型の汎化特化の制約を定義する概念でもある。ジェネリックなデータ構造、関数の型、クラスのメソッド、ジェネリックなクラス、ジェネリック関数などに適用されている。
Javaの場合、まず、非ジェネリック型の場合
なんのことだかさっぱりこれだけではわかりませんね。とりあえず、Javaの場合。とりあえず、このようなクラスがあると仮定します。動物は抽象的なクラス(親クラス、superクラス)。犬は動物を具象化したクラス(子クラス、subクラス)。になります。
/** 動物 */
class Animal {
}
/** 犬 */
class Dog extends Animal {
}
/** 猫 */
class Cat extends Animal {
}
これをお互いに代入しようとした場合、
// ※1
Animal animal = new Dog();
// ※2
Dog dog = new Animal(); // コンパイルエラー
Dog dog = (Dog)new Animal(); // キャストすれば代入可能だが、ClassCastExceptionの可能性が出てくる。
※1はできますが、※2はコンパイルエラーになります。Animalをinterfaceで書いても※1はできます。※2は文法的に書けません。
interface Animal {
public void cry();
}
class Dog implements Animal{
@Override
public void cry() {
System.out.println("Wooon");
}
}
Animal animal = new Dog();
Javaは、代入においてこのようなルールがあります。kotlinにおいても同じです。
- 代入する値がよりデータ幅の広い型(親クラス、superクラス)の変数に代入する場合はJavaが自動的にキャストを行う。
- 代入する値がよりデータ幅の狭い型(子クラス、subクラス)の変数に代入する場合はエラーになるため、プログラマによるキャストが必要
Javaの場合、ジェネリック型の場合
ジェネリック(generic)型の場合はどうなるでしょうか?ジェネリック型はJavaではJava5で導入されています。
// ※1
List<Animal> animalList = new ArrayList<Dog>(); // コンパイルエラー
// ※2
List<Dog> dogList = new ArrayList<Animal>(); // コンパイルエラー
非ジェネリック型でできた※1もコンパイルエラーになり、※1も※2も両方コンパイルエラーになります。ジェネリック型の場合は非ジェネリック型と違って、ジェネリックの型が一致していないと代入できません。
これを不変 (invariant)と言います。
// ※1
List<Animal> animalList = new ArrayList<Animal>();
// ※2
List<Dog> dogList = new ArrayList<Dog>();
そこで、「共変性」と「反変性」の話が出てくる
ここで、ようやく「共変性」と「反変性」の話がでてくるのですが、
- 共変 (covariant): 広い型(親クラス、superクラス)から狭い型(子クラス、subクラス)へ変換すること。
- 反変 (contravariant) : 狭い型(子クラス、subクラス)から広い型(親クラス、superクラス)へ変換すること。
Javaの場合の「共変性」と「反変性」の表現
Javaの場合は、< >の中のジェネリック型の表記にextends、superを使います。
// ※1
List<? extends Animal> animalList = new ArrayList<Dog>();
// ※2
List<? super Dog> dogList = new ArrayList<Animal>();
※1は共変。左辺の代入される側が広い型(Animal)から狭い型(Dog)に変換されています。Animal → Dogに型変換だからextends。これはしっくりきます。
※2は反変。逆に左辺の代入される側が狭い型(Dog)から広い型(Animal)に変換されています。Dog → Animalに型変換だからsuper。これはしっくりきます。
Kotlinの場合の「共変性」と「反変性」の表現
これがkotlinだとどうなるでしょうか?kotlinの場合はこれらをin、outを使って表現するのだが、Javaと違ってややこしい。理解としてあっているかどうか、はなはだ怪しいが、上記のJavaの場合と同じことやろうとするとこうなる。
共変(covariant)の場合
fun hoge() {
val animalList: MutableList<out Animal> = mutableListOf<Dog>()
}
上はコレクションフレームワークの例だが、独自クラスの場合、コストラクタの引数のプロパティは変更できない。読み取りのみになる。
class Biology<out T: Animal>(_content: T) {
val content = _content // varにできない
}
または
class Biology<out T: Animal>(val content: T) { // varにできない
}
fun test1() {
var animal = Biology(Animal())
var dog = Biology(Dog())
animal = dog // これはok
}
fun test2() {
var animal = Biology(Animal())
var dog = Biology(Dog())
dog = animal // コンパイルエラー
}
反変(contravariant)の場合
fun hoge() {
val dogList: MutableList<in Dog> = mutableListOf<Animal>()
}
独自クラスの場合、コンストラクタの引数のプロパティはprivateでなければならない。外からの参照は許されない。
class Biology<in T: Animal>(_content: T) {
private var content = _content // privateでなければいけない
}
または
class Biology<in T: Animal>(private var content: T) { // privateでなければいけない
}
fun test1() {
var animal = Biology(Animal())
var dog = Biology(Dog())
dog = animal // これはok
}
fun test2() {
var animal = Biology(Animal())
var dog = Biology(Dog())
animal = dog // コンパイルエラー
}
まとめ
Javaの共変=extend、反変=superは名前的にもしっくりくるが、kotlinのout、inは名前の付け方がイマイチしっくりこない。(何で、inなのか?何で、outなのか?)
kotlinは内部的にはJavaのソースコードに変換され、同じようにJVM上で動作するので、振る舞い的にはJavaもKotlinも同じであろうと思う。kotlinにはkotlinの何かポリシーがあったんだろうか・・・?
と、言うわけで、JavaとKotlinの共変、反変について簡単にまとめると以下のようになるが、色々なサイトで共変、反変の説明を見ても、これだという決定打がなく。イマイチ自分でも煮えきらないので、何か補足があればコメントよろしくおねがいします。
java | kotlin | プロパティ | ||
---|---|---|---|---|
共変(covariant) | 広い型から狭い型へ変換 | extends | out | 変更不可(val) |
反変(contravariant) | 狭い型から広い型へ変換 | super | in | 外から参照不可(private) |
ps.コメント頂きました
- 共変(covariant)の場合は、上記の例の通り、コンストラクタの引数がvalで参照のみとなります。参照のみなので、出力のみ、なのでout。
- 反変(contravariant)の場合は、上記の例の通り、コンストラクタの引数はprivate varとなります。変更のみなので、に入力のみなのでin。
ちょっと、覚えづらいですね。
zennにいい記事がありました。
筒で理解する反変・共変
見て理解するジェネリクス 基礎から共変・反変までバッチリ習得