Kotlin: Kotlin泛型详解
最后更新:2026-08-26
Kotlin 的泛型比 Java 更安全——声明处型变(declaration-site variance)让 Charlie 在定义 时就决定协变,而不是在每个使用点重复写 。
1. 你将学到
- 型变:(协变)vs (逆变)
- 使用处型变:类型投影
- 星投影: 未知泛型的安全访问
- :内联函数保留泛型类型信息
- Charlie 实战: /
2. 一个架构师的真实故事
(1) 痛点:Java 泛型通配符地狱
Charlie 在 Java 中写 表示只读订单列表, 表示只写列表。每次使用都要重复声明通配符,代码可读性极差。
(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) 型变规则速查
| 修饰符 | 含义 | T 出现位置 | 子类型方向 | 类比 |
|---|---|---|---|---|
| 协变 | 只在返回位置 | 是 的子类型 | Java | |
| 逆变 | 只在参数位置 | 是 的子类型 | Java | |
| 无修饰 | 不变 | 任意位置 | 无子类型关系 | Java |
(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 |
|---|---|---|
| 只读参数 | ||
| 只写参数 | ||
| 定义处协变 | 不支持 | |
| 定义处逆变 | 不支持 |
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) 星投影规则
| 类型 | 星投影等价 | 可读 | 可写 |
|---|---|---|---|
| ✅ (Any?) | ❌ | ||
| ✅ (Any?) | ❌ | ||
| ✅ (Any?) | ❌ | ||
| ❌ | ✅ (Nothing = 不可写) |
7. reified 类型参数
(1) 类型擦除与 reified
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) reified 实用场景
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 或不变之一。如果 T 同时出现在输入和输出位置,只能是不变。
Q Kotlin 的泛型在运行时还存在吗?
A 大部分情况下被擦除了(和 Java 一样)。但 inline + reified 可以保留类型信息。内联函数在调用处展开,类型信息在编译时可用。
Q 星投影 和 有什么区别?
A 表示"元素类型未知",只能读为 Any?; 表示"元素类型是 Any",可以添加任何值。前者更安全。
Q 什么时候用声明处型变,什么时候用使用处?
A 优先声明处型变——在设计 API 时就确定 T 是只读(out)还是只写(in)。使用处型变是最后的手段。
Q reified 有性能开销吗?
A 没有额外运行时开销。因为 inline 函数在编译时展开,reified 类型检查在编译期完成,运行时无额外操作。
Q 泛型上界约束 和 子句有什么区别?
A 单个约束用 简写;多个约束必须用 语法。
📖 小节
- (协变):T 只出现在返回位置, 是 子类型
- (逆变):T 只出现在参数位置, 是 子类型
- 声明处型变一次定义,使用处型变(投影)按需临时指定
- 星投影 用于泛型未知场景,安全但受限
- 在 inline 函数中保留泛型类型信息,实现运行时类型检查
- 型变口诀:Producer → out,Consumer → in
📝 作业
- 基础题(难度⭐):实现一个
<T>,测试不同类型参数的赋值兼容性。提示: 能赋给 吗? - 进阶题(难度⭐⭐):定义 和 ,演示协变和逆变的赋值关系。提示: →
- 挑战题(难度⭐⭐⭐):实现一个
<Any>函数,先过滤类型再转换。提示:<T>