7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

共変性と反変性、JavaとKotlinの場合

Last updated at Posted at 2023-03-12

共変性と反変性とはなんぞや?

共変性と反変性について、うろ覚えだったので改めて再確認し、忘れないようにここに書いておきます。
「共変性」と「反変性」言葉としてはあまり日常的にはでてきませんが、無意識のうちにつかっています。

理解できているようでなかなか理解ができていない、説明するのも難しいです。

「共変性」と「反変性」とは、Wikipediaから

(コンピュータプログラミングの型システムでの)共変性と反変性(きょうへんせいとはんぺんせい、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にできない
}

varに変更しようとするとコンパイルエラーになる。
Screenshot_20230312_172108.png

    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でなければいけない
}

publicにしようとするとコンパイルエラーになる
Screenshot_20230312_172651.png

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にいい記事がありました。
筒で理解する反変・共変
見て理解するジェネリクス 基礎から共変・反変までバッチリ習得

7
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?