Kotlin: Kotlinのジェネリック
最終更新:2026-08-26
KotlinのジェネリクスはJavaのものよりも安全です。宣言箇所でのバリアンスにより、Charlieは使用箇所ごとにワイルドカードを繰り返す必要がなく、定義時に共変性を決定することができます。
1. 学習内容
- 分散:
out(共変)対in(反変) - 使用場所による差異:タイプ予測
- スター投影:
*未知のジェネリックへの安全なアクセス reified: インライン関数はジェネリック型の情報を保持する- チャリーの活躍:
ReadOnlyRepository<out T>/OrderFilter<in T>
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) 分散フロー図
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 という構文を使用する必要があります。📖 まとめ
out(共変):Tは戻り位置にのみ現れます。Producer<Sub>はProducer<Base>のサブタイプです。in(反変):Tはパラメータの位置にのみ現れます。Consumer<Base>はConsumer<Sub>のサブタイプです。- 「宣言サイト」の変動は一度だけ定義され、「使用サイト」の変動(予測)はオンデマンドで上書きされる
- スター・プロジェクション
*は、未知の一般的なシナリオを対象としています。安全ですが、適用範囲は限られています。 reifiedは、インライン関数内のジェネリック型情報を保持し、実行時の型チェックを可能にします- 分散の覚え方:プロデューサー → アウト、コンシューマー → イン
📝 練習問題
- 初心者 (⭐): 汎用的な
Box<T>を実装し、さまざまな型引数との代入互換性をテストしてください。ヒント:Box<Int>をBox<Any>に代入することはできますか? - 中級 (⭐⭐):
ReadOnlyRepository<out T>およびWriteRepository<in T>を定義し、共変性および反変の割り当て関係を示してください。ヒント:List<BulkOrder>→List<out Order> - 上級 (⭐⭐⭐): 型でフィルタリングしてから型変換を行う
spotNull<reified Any>関数を実装してください。ヒント:filterIsInstance<T>を使用してください。