はじめに
Kotlin では ジェネリクス (Generics) を使うことで、
型に依存しない柔軟で安全なコード を書けます。
Java のジェネリクスを継承しつつ、Kotlin らしい表現も可能です。
1. ジェネリクスとは?
ジェネリクスとは 型をパラメータとして扱う仕組み です。
コレクションを例にするとわかりやすいです。
val numbers: List<Int> = listOf(1, 2, 3)
val names: List<String> = listOf("Alice", "Bob")
List<T> の T がジェネリクス。
List<Int> や List<String> のように型を指定することで 型安全 を保証します。
2. ジェネリッククラス
自作のクラスにもジェネリクスを使えます。
class Box<T>(val value: T)
val intBox = Box(10)
val stringBox = Box("Hello")
println(intBox.value) // 10
println(stringBox.value) // Hello
Box<T> と宣言し、利用時に Box<Int> や Box<String> を指定します。
3. ジェネリック関数
関数にも型パラメータを導入できます。
fun <T> identity(value: T): T {
return value
}
println(identity(42)) // Int
println(identity("Kotlin")) // String
<T> を関数の先頭に書きます。
呼び出し時に型を指定することも可能です。
val result = identity<String>("Hello")
4. 型境界 (型制約: Upper Bounds)
ジェネリクスに 「この型のサブクラスだけ許可」 という制約を付けられます。
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
println(sum(10, 20)) // 30.0
println(sum(3.5, 2.2)) // 5.7
// println(sum("a", "b")) // コンパイルエラー
T : Number と書くと、T は Number かそのサブクラスである必要があります。
5. 共変 (out) と反変 (in)
Kotlin のジェネリクスでは 型の変性 (variance) を明示できます。
共変 (out)
「読み取り専用の型パラメータ」
Producer<out T> は T を生成する(出す)だけ の場合に使います。
class Producer<out T>(private val value: T) {
fun produce(): T = value
}
val producer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = producer // 共変なので代入可能
Producer<String> を Producer<Any> に代入できる。
反変 (in)
「書き込み専用の型パラメータ」
Consumer<in T> は T を消費する(受け取る)だけ の場合に使います。
class Consumer<in T> {
fun consume(value: T) {
println("Consumed: $value")
}
}
val consumer: Consumer<Number> = Consumer()
val intConsumer: Consumer<Int> = consumer // 反変なので代入可能
intConsumer.consume(10) // Consumed: 10
Consumer<Number> を Consumer<Int> に代入できる。
6. 型消去 (Type Erasure)
Kotlin(JVM)では実行時にはジェネリクスの型情報が消えます。
fun <T> printType(list: List<T>) {
if (list is List<String>) { // 警告あり: 実行時には判断できない
println("String list")
}
}
実行時には List<String> も List<Int> も単なる List になるため、型チェックはできません。
7. reified 型パラメータ(インライン関数)
インライン関数 (inline) と組み合わせると、実行時に型情報を保持できる ようになります。
inline fun <reified T> isType(value: Any): Boolean {
return value is T
}
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
reified によって実行時に型チェック可能になります。
まとめ
- ジェネリクスは「型をパラメータ化する仕組み」
- クラス・関数に
<T>を導入できる - 型制約 (
<T : Number>) で上限を設定可能 - 変性 (out / in) を明示することで安全な代入が可能
- 実行時は型消去されるが、
reifiedを使えば型を保持できる