Kotlinのジェネリクス(Generics)は、「型をパラメータとして抽象化する仕組み」です。
単なる再利用のための機能ではなく、型安全性をコンパイル時に最大化するための設計思想が随所に反映されています。
本記事では、
- 基本構文
- 型制約
- in / out(変性)
- スター投影
- reified と型消去
を仕様的に正確かつ実務で誤解しにくい形で整理します。
目次
ジェネリクスとは何か
ジェネリクスとは、型を引数として受け取る仕組みです。
fun <T> identity(value: T): T {
return value
}
Tは型パラメータ- 呼び出し時に具体的な型が決定される
- キャスト不要で型安全
val a = identity(10) // Int
val b = identity("Hello") // String
同じロジックを、型安全に再利用できるのが最大の利点です。
クラスのジェネリクス
class Box<T>(val value: T)
val intBox = Box(10) // Box<Int>
val stringBox = Box("Hi") // Box<String>
- クラス自体が型を持つ
Box<Int>とBox<String>は別物として扱われる
複数の型パラメータ
class PairBox<A, B>(
val first: A,
val second: B
)
val pair = PairBox(1, "One")
- 型パラメータは任意個指定可能
- Kotlin標準の
Pair<A, B>と同じ発想
型制約(上限境界)
単一制約
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
TはNumberのサブタイプに限定される- 制約内のメンバが安全に使える
複数制約(where句)
fun <T> printComparable(value: T)
where T : CharSequence,
T : Comparable<T> {
println(value)
}
Tは複数のインターフェースを同時に満たす必要がある- クラス継承 + インターフェース実装の制約を同時に表現可能
Null安全とジェネリクス
class Box<T>(val value: T)
Tは実質的にAny?上限nullが入る可能性を含む
nullを排除したい場合
class NonNullBox<T : Any>(val value: T)
Tは非null型に限定される- API設計では非常に重要なテクニック
Kotlinにおける「変性(Variance)」の核心
なぜ変性が必要か
open class Animal
class Dog : Animal()
val dogs: List<Dog> = listOf(Dog())
val animals: List<Animal> = dogs // OK
これは Kotlinの List が共変として定義されているため可能です。
宣言時変性(out / in)
out(共変)
interface Producer<out T> {
fun produce(): T
}
out T= Tを外へ出す専用- 返り値には使える
- 引数としては原則使えない
val dogProducer: Producer<Dog>
val animalProducer: Producer<Animal> = dogProducer
読み取り専用構造に適する
in(反変)
interface Consumer<in T> {
fun consume(value: T)
}
in T= Tを受け取る専用- 引数には使える
- 返り値には使えない
val animalConsumer: Consumer<Animal>
val dogConsumer: Consumer<Dog> = animalConsumer
書き込み専用構造に適する
List と MutableList の決定的な違い
List(読み取り専用)
interface List<out E>
- 宣言時点で
out - 共変なので
List<Dog>→List<Animal>が可能 - 要素の追加はできない
MutableList(可変)
interface MutableList<E>
- 変性指定なし(不変 / invariant)
- 書き込み・読み取り両方を行うため安全上共変にできない
val dogs: MutableList<Dog> = mutableListOf()
val animals: MutableList<Animal> = dogs // ❌ コンパイルエラー
use-site variance(使用箇所変性)
fun printAnimals(list: List<out Animal>) {
list.forEach { println(it) }
}
- 宣言時ではなく使用時に out / in を指定
- Javaの
<? extends Animal>に近い表現
スター投影(Star Projection)
val list: List<*> = listOf(1, "A", 3.0)
- 型引数が不明なジェネリクス
- 読み取り時は
Any? - 型安全を保つため制限が強い
MutableList<*> の注意点
- 要素型が不明な可変リスト
- 非null値は基本的に追加不可
- 「何が入るかわからない箱」と理解すると安全
型消去(Type Erasure)
fun <T> check(value: Any): Boolean {
return value is T // ❌ コンパイルエラー
}
- Kotlin/JVMでは実行時にジェネリクス情報が消える
List<Int>とList<String>は実行時に区別できない
reified(実体化された型パラメータ)
inline fun <reified T> isType(value: Any): Boolean {
return value is T
}
isType<String>("Hello") // true
inline関数限定- 型情報を呼び出し側に展開することで実行時判定を可能にする
- Kotlin特有の非常に強力な機能
実務でよく使われるジェネリクス例
interface Repository<T> {
fun save(item: T)
fun find(): T
}
- Repository
- UseCase
- Mapper
- Result / Either
クリーンアーキテクチャ・DI設計と相性が非常に良い
よくある誤解まとめ
| 誤解 | 正しい理解 |
|---|---|
| out は引数に絶対使えない | 原則不可だが UnsafeVariance 等の例外はある |
| MutableList は List の上位互換 | 設計思想が別物 |
| List<*> は List<Any?> | まったく別(unknown type) |
| ジェネリクスは実行時にも残る | JVMでは基本的に消える |
まとめ
- Kotlinのジェネリクスは 型安全を最大化するための設計
out / inは「できる・できない」ではなく「安全かどうか」ListとMutableListの違いは変性理解の核心reifiedは JVM制約を超えるための切り札
以上、Kotlinのジェネリクスについてでした。
最後までお読みいただき、ありがとうございました。










