はじめに
Kotlin のジェネリクスには 3 つの変性 (variance) モードがあります:
| 変性の種類 | Kotlin キーワード | Java 記法 | 意味 |
|---|---|---|---|
| 共変 | out |
? extends T |
出すだけ(読み取り専用) |
| 反変 | in |
? super T |
入れるだけ(書き込み専用) |
| 不変 | (なし) | T |
両方使えるが、代入不可(安全のため) |
1. 不変 (invariant) とは?
不変とは、**「型パラメータのサブタイプ関係が維持されない」**ことです。
つまり、
Box<String>とBox<Any>の間には継承関係がない。
class Box<T>(var value: T)
val stringBox: Box<String> = Box("Hello")
// val anyBox: Box<Any> = stringBox // ❌ コンパイルエラー
Box<T> は不変なので、型を変えて代入することはできません。
2. なぜ不変なのか?
もし Box<String> を Box<Any> として扱えたら、
次のような危険なコードが通ってしまいます。
class Box<T>(var value: T)
val stringBox: Box<String> = Box("Hello")
val anyBox: Box<Any> = stringBox // 仮に許可されたとしたら...
anyBox.value = 123 // Int を代入
println(stringBox.value) // String じゃなくなってしまう!
これが 型安全性の破壊。
Kotlin はこの危険を避けるために、
T を invariant(不変) にすることで「代入禁止」にしています。
3. 不変はデフォルト
Kotlin のジェネリクスは、明示しない限りすべて不変 です。
class Container<T>(val item: T)
val a: Container<String> = Container("A")
// val b: Container<Any> = a // ❌ 不変なので代入できない
不変をベースにして、必要に応じて out または in を明示する設計が Kotlin の基本思想です。
4. 使い分けの指針
| シーン | 修飾子 | 例 | 意味 |
|---|---|---|---|
| 出力専用(返すだけ) | out |
List<out T> |
読み取り専用。共変。 |
| 入力専用(受け取るだけ) | in |
MutableList<in T> |
書き込み専用。反変。 |
| 入力・出力どちらも使う | (なし) | MutableList<T> |
不変(安全のため代入禁止)。 |
5. 例:MutableList<T> は不変
val strings: MutableList<String> = mutableListOf("A", "B")
// val anys: MutableList<Any> = strings // ❌ 不変なので代入不可
MutableList<T> は 読み書き両方できる ため、
安全性を保つために 不変 (invariant) に設計されています。
6. 関数型との関係
関数型 (A) -> B は、入力と出力が自動的に変性指定されています:
| 部分 | 変性 | 意味 |
|---|---|---|
| 引数(A) |
in(反変) |
消費する |
| 戻り値(B) |
out(共変) |
生み出す |
逆に、クラスの型パラメータが入出力両方に使われている 場合は不変になります。
class Transformer<T> {
fun input(value: T) { /* in */ }
fun output(): T { /* out */ TODO() }
}
T が in/out の両方に使われているため、安全性のために不変でなければなりません。
まとめ
| 概念 | Kotlin 記法 | 意味 | 代入関係 |
|---|---|---|---|
| 共変 (Covariant) | out |
出力専用 | サブタイプ可(Producer<String> → Producer<Any>) |
| 反変 (Contravariant) | in |
入力専用 | 逆サブタイプ可(Consumer<Number> → Consumer<Int>) |
| 不変 (Invariant) | なし | 入出力両方 | 代入不可(安全性優先) |