跳到主要内容

Kotlin 泛型

问题

Kotlin 泛型有哪些特性?inoutreified 各有什么作用?

答案

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 Tin TT
T 用途只做返回值只做参数都可以
子类型方向同向(Dog→Animal)反向(Animal→Dog)无关系
助记Producer(生产者)Consumer(消费者)既生产又消费
Java 对应? extends T? super TT

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 outJava ? 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 有什么限制?

答案

  1. 只能用在 inline 函数上 — 非 inline 函数无法保留类型信息
  2. 不能用在类的类型参数上class Box<reified T> 不合法
  3. 不能创建 reified 类型的实例T() 不行(除非加 T : () -> Unit 约束)
  4. 会增加代码体积 — 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 依赖注入的底层思路。

相关链接