Kotlin 泛型
问题
Kotlin 泛型有哪些特性?in、out、reified 各有什么作用?
答案
1. 泛型基础
// 泛型类
class Box<T>(val value: T)
// 泛型函数
fun <T> singletonList(item: T): List<T> = listOf(item)
// 泛型约束
fun <T : Comparable<T>> sort(list: List<T>) { /* ... */ }
// 多约束用 where
fun <T> ensureValid(item: T) where T : Serializable, T : Comparable<T> {
// T 同时实现 Serializable 和 Comparable
}
2. 型变(Variance)
这是 Kotlin 泛型的核心考点。型变决定了泛型类型之间的子类型关系。
协变 out(生产者)
// out T —— 只能作为输出(返回值),不能作为输入(参数)
interface Producer<out T> {
fun produce(): T // ✅ T 作为返回值
// fun consume(item: T) // ❌ 编译错误,不能作为参数
}
// List 声明为 List<out E>,所以是协变的
val strings: List<String> = listOf("a", "b")
val objects: List<Any> = strings // ✅ List<String> 是 List<Any> 的子类型
逆变 in(消费者)
// in T —— 只能作为输入(参数),不能作为输出(返回值)
interface Consumer<in T> {
fun consume(item: T) // ✅ T 作为参数
// fun produce(): T // ❌ 编译错误,不能作为返回值
}
// Comparable 声明为 Comparable<in T>
val comparator: Comparable<Number> = object : Comparable<Number> {
override fun compareTo(other: Number) = 0
}
val intComparator: Comparable<Int> = comparator // ✅ Comparable<Number> 是 Comparable<Int> 的子类型
PECS 原则
out(协变) | in(逆变) | 不变 | |
|---|---|---|---|
| 关键字 | out T | in T | T |
| T 用途 | 只做返回值 | 只做参数 | 都可以 |
| 子类型方向 | 同向(Dog→Animal) | 反向(Animal→Dog) | 无关系 |
| 助记 | Producer(生产者) | Consumer(消费者) | 既生产又消费 |
| Java 对应 | ? extends T | ? super T | T |
3. 声明处型变 vs 使用处型变
// 声明处型变 —— 在类定义时声明(Kotlin 风格)
class ReadOnlyBox<out T>(val value: T) // 永远是协变的
// 使用处型变 —— 在使用时声明(等价于 Java 通配符)
fun copy(from: Array<out Any>, to: Array<Any>) {
for (i in from.indices) {
to[i] = from[i]
}
}
4. 星投影 *
当不关心具体类型参数时使用 *:
// * 等价于 out Any?(对于协变类型参数)
fun printList(list: List<*>) {
list.forEach { println(it) } // 每个元素类型为 Any?
}
// MutableList<*> 等价于 MutableList<out Any?>
// 可以读取(Any?),但不能写入(Nothing)
5. reified 类型参数
普通泛型在运行时会被类型擦除,reified 配合 inline 可保留类型信息:
// ❌ 普通泛型 —— 运行时不知道 T 是什么
fun <T> isInstanceOf(value: Any): Boolean {
// return value is T // 编译错误!类型擦除了
return false
}
// ✅ reified —— 运行时保留类型信息
inline fun <reified T> isInstanceOf(value: Any): Boolean {
return value is T // ✅ 可以检查类型
}
isInstanceOf<String>("hello") // true
isInstanceOf<Int>("hello") // false
// Android 实战:简化 Intent 启动
inline fun <reified T : Activity> Context.startActivity() {
startActivity(Intent(this, T::class.java))
}
// 使用
startActivity<DetailActivity>() // 无需传 Class 对象
// JSON 解析
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java)
}
val user = gson.fromJson<User>(jsonString)
为什么 reified 只能用于 inline 函数?
inline 函数的代码会被复制到调用处。编译器在每个调用处知道具体的类型参数,可以直接替换为具体类型。非 inline 函数在编译后只有一份代码,无法持有多个"调用处的类型信息"。
常见面试问题
Q1: Kotlin 的 out vs Java 的 ? extends 有什么异同?
答案:
功能等价,但使用方式不同:
| 特性 | Kotlin out | Java ? extends |
|---|---|---|
| 位置 | 声明处(类定义)或使用处 | 只能在使用处 |
| 语法 | class Box<out T> | Box<? extends T> |
| 可读性 | 更直观(out = 输出) | 通配符语法较绕 |
Kotlin 的声明处型变一次声明处处生效,而 Java 每次使用都要写 ? extends。
Q2: 为什么 Array<String> 不能赋值给 Array<Any>?
答案:
因为 Array 是不变的(invariant):
val strings: Array<String> = arrayOf("a", "b")
// val objects: Array<Any> = strings // ❌ 编译错误
// 如果允许,就可以写入非 String:
// objects[0] = 42 // 运行时 ArrayStoreException!
Array 既可以读又可以写,如果允许协变就会类型不安全。而 List<out E> 是只读的,所以可以协变。
Q3: reified 有什么限制?
答案:
- 只能用在
inline函数上 — 非 inline 函数无法保留类型信息 - 不能用在类的类型参数上 —
class Box<reified T>不合法 - 不能创建 reified 类型的实例 —
T()不行(除非加T : () -> Unit约束) - 会增加代码体积 — inline 函数在每个调用处复制一份代码
Q4: 什么是类型擦除?有什么影响?
答案:
JVM 不支持运行时泛型信息,编译后所有泛型参数被替换为上界(通常是 Object):
// 编译前
fun <T> getList(): List<T> = listOf()
// 编译后(类型擦除)
fun getList(): List<Object> = listOf()
影响:
- 不能 在运行时判断泛型类型:
list is List<String>❌ - 不能 创建泛型数组:
Array<T>()❌ - 可以 判断擦除后的类型:
list is List<*>✅ - 用
reified可以绕过部分限制
Q5: 如何设计一个类型安全的异构容器?
答案:
class TypeSafeMap {
private val map = mutableMapOf<KClass<*>, Any>()
fun <T : Any> put(clazz: KClass<T>, value: T) {
map[clazz] = value
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> get(clazz: KClass<T>): T? {
return map[clazz] as? T
}
}
// 使用
val container = TypeSafeMap()
container.put(String::class, "Hello")
container.put(Int::class, 42)
val str: String? = container.get(String::class) // "Hello"
val num: Int? = container.get(Int::class) // 42
用 KClass 作为 key 来保证类型安全,这也是 Hilt/Dagger 依赖注入的底层思路。