Kotlin: Kotlinのジェネリック

最終更新:2026-08-26

KotlinのジェネリクスはJavaのものよりも安全です。宣言箇所でのバリアンスにより、Charlieは使用箇所ごとにワイルドカードを繰り返す必要がなく、定義時に共変性を決定することができます。

1. 学習内容


2. 本物の建築家の物語

(1) 課題:Javaのジェネリックワイルドカード地獄

Charlieは、Javaで読み取り専用オーダーリストにはList<? extends Order>を、書き込み専用リストにはList<? super Order>を記述しています。どの使用箇所でもワイルドカードを繰り返して記述する必要があるため、コードがほとんど読めなくなってしまいます。

(2) Kotlinにおける宣言型バリアンスの解決策

JAVA
// Java: repeat wildcard at every use site
void process(List<? extends Order> orders) { }
void addAll(List<? super Order> target) { }

// Kotlin: declare variance once at definition site
class Repository<out T : Order> {  // Covariant at definition
    fun getAll(): List<T>  // T only appears in 'out' position
}
// No wildcards needed at use site!

宣言時の分散は一度定義すれば何度でも利用できます。一方、Javaの使用場所分散は呼び出し箇所ごとに記述する必要があります。


3. ジェネリクスの基礎

(1) 汎用クラスと関数

KOTLIN
// Generic class
class Box<T>(val value: T) {
    fun unwrap(): T = value
}

val intBox = Box(42)         // T inferred as Int
val strBox = Box("Hello")   // T inferred as String

// Generic function
fun <T> singletonList(item: T): List<T> = listOf(item)

// Generic with type constraint
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

// Multiple constraints
fun <T> process(item: T) where T : CharSequence, T : Comparable<T> {
    // T is both CharSequence and Comparable
}

(2) 分散の問題:なぜ「out/in」が必要なのか

KOTLIN
// This seems reasonable but is a TYPE ERROR
val strings: List<String> = listOf("Hello", "World")
// val objects: List<Any> = strings  // ERROR in Kotlin (OK in Java arrays)

// Why? If allowed:
// objects.add(42)  // Would corrupt the String list!
// strings[0].length  // ClassCastException at runtime

4. 申告内容と実態の相違

(1) out(共変):プロデューサーのみ

KOTLIN
// Producer: T only appears in 'out' position (return type)
interface Repository<out T> {
    fun findById(id: String): T
    fun findAll(): List<T>
    // fun save(item: T)  // ERROR: T in 'in' position
}

// Now this works:
val orderRepo: Repository<Order> = OrderRepositoryImpl()
val anyRepo: Repository<Any> = orderRepo  // OK! Covariant

(2) in(反変):コンシューマーのみ

KOTLIN
// Consumer: T only appears in 'in' position (parameter type)
interface Filter<in T> {
    fun test(item: T): Boolean
    fun filter(items: List<T>): List<T>
    // fun getResult(): T  // ERROR: T in 'out' position
}

// Now this works:
val orderFilter: Filter<Order> = object : Filter<Order> {
    override fun test(item: Order) = item.total > 1_000
    override fun filter(items: List<Order>) = items.filter { test(it) }
}
val anyFilter: Filter<Any> = orderFilter  // Cannot assign - contravariant direction

(3) 分散に関する規則の概要

修飾子 意味 登場作品 サブタイプの方向 アナログ
out 共変 戻り値の位置でのみ Producer<Sub>Producer<Base> のサブタイプ Java ? extends
in 反変 パラメータ位置でのみ Consumer<Base>Consumer<Sub> のサブタイプ Java ? super
なし 不変 任意の位置 サブタイプ関係なし Java T

(4) 分散フロー図

100%
flowchart TD
    subgraph "Covariant (out)"
        A1[Producer of Sub] -->|"IS-A"| A2[Producer of Base]
    end
    subgraph "Contravariant (in)"
        B1[Consumer of Base] -->|"IS-A"| B2[Consumer of Sub]
    end
    subgraph "Invariant"
        C1[Box of Sub] -.->|"NOT IS-A"| C2[Box of Base]
    end

5. 用途・敷地差異(タイプ予測)

宣言場所で変動量を決定できない場合は、使用場所でそれを予測してください。

KOTLIN
// Use-site projection: when declaration-site is invariant
fun copyFrom(source: Array<out Any>, dest: Array<in Any>) {
    for (i in source.indices) {
        dest[i] = source[i]
    }
}

// Equivalent to Java:
// void copyFrom(Object[] source, Object[] dest)

(1) 使用場所ごとのばらつきの比較

シナリオ Java Kotlin
読み取り専用パラメータ <? extends T> <out T>
書き込み専用パラメータ <? super T> <in T>
宣言の共変性 未対応 out T
反変宣言 未対応 in T

6. 星の投影

ジェネリック型が不明な場合は、安全にアクセスするためにスター投影を使用してください。

KOTLIN
// List<*> = list of unknown type (safe to read, not write)
val unknownList: List<*> = listOf("Hello", 42, 3.14)
val first: Any? = unknownList[0]  // OK - returns Any?
// unknownList.add("New")  // ERROR - can't write

// Comparison
fun printSize(list: List<*>) {
    println("Size: ${list.size}")  // OK - size doesn't depend on T
}

// Array<*> is different
val array: Array<*> = arrayOf("A", "B")
// array[0] = "C"  // ERROR - can't write to Array<*>
val elem: Any? = array[0]  // OK - can read

(1) 星の投影に関する規則

タイプ スター投影相当 読み取り可能 書き込み可能
Foo<out T> Foo<out Any?> ✅ (ある?)
Foo<in T> Foo<in Nothing> ✅ (なし = 書き込み不可)
Foo<T>(不変、T 上限) Foo<out Any?> ✅(任意?)
Foo<T> (T の上限 T : Upper) Foo<out Upper> ✅ (上限)

7. 実体化された型パラメータ

(1) 型の消去と実体化

KOTLIN
// Normal generic: type is erased at runtime
fun <T> isA(value: Any): Boolean = value is T  // ERROR: Cannot check for instance of erased type

// reified: inline function preserves type at call site
inline fun <reified T> isA(value: Any): Boolean = value is T  // OK!

// Usage
isA<String>("Hello")  // true
isA<Int>(42)          // true
isA<Int>("42")        // false

// Practical: filter by type
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

(2) 実用的な具体化されたユースケース

KOTLIN
// JSON deserialization without Class parameter
inline fun <reified T> Json.decodeFromString(json: String): T

// Instead of:
// json.decodeFromString(Order::class.java, jsonString)

// Just:
// json.decodeFromString<Order>(jsonString)

8. 完全な例:型安全なオーダーリポジトリ

▶ サンプル:型安全な注文リポジトリ

KOTLIN
// ============================================
// OrderProcessor - Type-Safe Repository with Generics
// Feature: Generic repository with variance
// ============================================

open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)
class PriorityOrder(id: String, total: Double, val priority: String) : Order(id, total)

// Covariant repository: only produces T
interface ReadOnlyRepository<out T : Order> {
    fun findById(id: String): T?
    fun findAll(): List<T>
}

// Contravariant writer: only consumes T
interface WriteRepository<in T : Order> {
    fun save(item: T)
    fun saveAll(items: List<T>)
}

// Full repository: invariant (both reads and writes)
interface OrderRepository<T : Order> : ReadOnlyRepository<T>, WriteRepository<T>

// In-memory implementation
class InMemoryOrderRepository<T : Order> : OrderRepository<T> {
    private val storage = mutableMapOf<String, T>()

    override fun findById(id: String): T? = storage[id]
    override fun findAll(): List<T> = storage.values.toList()
    override fun save(item: T) { storage[item.id] = item }
    override fun saveAll(items: List<T>) { items.forEach { save(it) } }
}

// Generic filter with contravariance
class OrderFilter<in T : Order>(private val predicate: (T) -> Boolean) {
    fun test(item: T): Boolean = predicate(item)
    fun filter(items: List<T>): List<T> = items.filter(predicate)
}

inline fun <reified T : Order> List<Order>.filterByType(): List<T> {
    return this.filterIsInstance<T>()
}

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99),
        BulkOrder("ORD-002", 5_000.00, 100),
        PriorityOrder("ORD-003", 15_000.00, "VIP"),
        BulkOrder("ORD-004", 8_000.00, 50),
        PriorityOrder("ORD-005", 2_500.00, "GOLD")
    )

    // Covariant: BulkOrder repo assignable to Order repo
    val bulkRepo: ReadOnlyRepository<BulkOrder> = InMemoryOrderRepository()
    val orderRepo: ReadOnlyRepository<Order> = bulkRepo  // OK: covariant

    // reified: filter by runtime type
    val bulkOrders: List<BulkOrder> = orders.filterByType<BulkOrder>()
    println("Bulk orders: ${bulkOrders.map { it.id }}")

    val priorityOrders: List<PriorityOrder> = orders.filterByType<PriorityOrder>()
    println("Priority orders: ${priorityOrders.map { it.id }}")

    // Generic filter
    val highValueFilter = OrderFilter<Order> { it.total > 1_000 }
    val highValue = highValueFilter.filter(orders)
    println("High value orders: ${highValue.map { it.id }}")
}

出力:

TEXT 📖 参照専用
Bulk orders: [ORD-002, ORD-004]
Priority orders: [ORD-003, ORD-005]
High value orders: [ORD-002, ORD-003, ORD-004, ORD-005]

❓ よくある質問

Q 「out」と「in」を同じ型パラメータに対して同時に指定することはできますか?
A いいえ。型パラメータは、「out」、「in」、または「invariant」のいずれかのみを指定できます。Tが入力位置と出力位置の両方に現れる場合、Tは「invariant」でなければなりません。
Q Kotlinのジェネリックは実行時にも存在しますか?
A ほとんどは消去されます(Javaと同様です)。ただし、インライン化およびリファイ化を行うことで、型情報を保持することができます。インライン関数は呼び出し箇所で展開されるため、型情報はコンパイル時に利用可能です。
Q 星型投影 *Any の違いは何ですか?
A List<*> は「要素型が不明」を意味し、Any? としてのみ読み取れます。 List<Any>は「要素型がAny」を意味し、任意の値を追加できます。前者のほうが安全です。
Q 宣言時のバリアンスと使用時のバリアンスは、それぞれどのような場合に使用すべきですか?
A 宣言時のバリアンスを優先してください。T が読み取り専用(out)か書き込み専用(in)かは、API 設計時に決定します。使用時のバリアンスは、あくまで最後の手段としてください。
Q 「reified」にはパフォーマンス上のオーバーヘッドはありますか?
A 実行時の追加のオーバーヘッドはありません。インライン関数はコンパイル時に展開されるため、「reified」の型チェックもコンパイル時に行われ、実行時に余分な処理は発生しません。
Q <T : Upper>where T : Upper の違いは何ですか?
A 制約が1つの場合は <T : Upper> という省略形を使用します。制約が複数ある場合は、where という構文を使用する必要があります。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): 汎用的な Box<T> を実装し、さまざまな型引数との代入互換性をテストしてください。ヒント:Box<Int>Box<Any> に代入することはできますか?
  2. 中級 (⭐⭐)ReadOnlyRepository<out T> および WriteRepository<in T> を定義し、共変性および反変の割り当て関係を示してください。ヒント:List<BulkOrder>List<out Order>
  3. 上級 (⭐⭐⭐): 型でフィルタリングしてから型変換を行う spotNull<reified Any> 関数を実装してください。ヒント: filterIsInstance<T> を使用してください。

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%