Kotlinのジェネリクスについて

採用はこちら

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()
}
  • TNumber のサブタイプに限定される
  • 制約内のメンバが安全に使える

複数制約(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 は「できる・できない」ではなく「安全かどうか」
  • ListMutableList の違いは変性理解の核心
  • reified は JVM制約を超えるための切り札

以上、Kotlinのジェネリクスについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次