1. 概述
Kotlin 的泛型与 Java 类似,但 Kotlin 在设计时做了更直观的改进,引入了 out
和 in
等关键字,使泛型的使用更清晰易懂。
泛型在实际开发中非常常见,比如集合类、工具类、扩展函数等都需要泛型支持。本文将从参数化类、类型投影、泛型约束、运行时类型擦除等多个角度,带你深入了解 Kotlin 中的泛型机制。
2. 创建参数化类
在 Kotlin 中,创建一个泛型类非常简单:
class ParameterizedClass<A>(private val value: A) {
fun getValue(): A {
return value
}
}
使用时可以显式指定泛型类型:
val parameterizedClass = ParameterizedClass<String>("string-value")
val res = parameterizedClass.getValue()
assertTrue(res is String)
也可以省略泛型类型,由编译器自动推断:
val parameterizedClass = ParameterizedClass("string-value")
val res = parameterizedClass.getValue()
assertTrue(res is String)
✅ 提示: Kotlin 的类型推断非常强大,大多数情况下不需要显式声明泛型。
3. Kotlin 中的 out
与 in
关键字
Kotlin 泛型中两个核心关键字是 out
和 in
,它们用于声明类型参数的协变(covariant)与逆变(contravariant)。
3.1. out
关键字:生产者(Producer)
当一个类只返回某个泛型类型的数据时,使用 out
声明,表示该类型只能被“产出”,不能被“消费”。
class ParameterizedProducer<out T>(private val value: T) {
fun get(): T {
return value
}
}
此时可以将 ParameterizedProducer<String>
赋值给 ParameterizedProducer<Any>
:
val parameterizedProducer = ParameterizedProducer("string")
val ref: ParameterizedProducer<Any> = parameterizedProducer
assertTrue(ref is ParameterizedProducer<Any>)
⚠️ 注意: 如果不加 out
,上述赋值会编译失败。
3.2. in
关键字:消费者(Consumer)
与 out
相反,in
表示该类型只能被“消费”,不能被“产出”。
class ParameterizedConsumer<in T> {
fun toString(value: T): String {
return value.toString()
}
}
此时可以将 ParameterizedConsumer<Number>
赋值给 ParameterizedConsumer<Double>
:
val parameterizedConsumer = ParameterizedConsumer<Number>()
val ref: ParameterizedConsumer<Double> = parameterizedConsumer
assertTrue(ref is ParameterizedConsumer<Double>)
⚠️ 注意: 如果不加 in
,上述赋值也会失败。
4. 类型投影(Type Projections)
有时候我们并不关心泛型的具体类型,只关心它是否是某个类型的子类或父类。Kotlin 提供了类型投影来解决这类问题。
4.1. 将子类型数组复制到父类型数组中
fun copy(from: Array<out Any>, to: Array<Any?>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
使用示例:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any?> = arrayOfNulls(3)
copy(ints, any)
assertEquals(any[0], 1)
assertEquals(any[1], 2)
assertEquals(any[2], 3)
4.2. 向父类型数组添加子类型元素
fun fill(dest: Array<in Int>, value: Int) {
dest[0] = value
}
使用示例:
val objects: Array<Any?> = arrayOfNulls(1)
fill(objects, 1)
assertEquals(objects[0], 1)
4.3. 星号投影(Star Projection)
当我们不关心泛型的具体类型时,可以使用 *
通配符:
fun printArray(array: Array<*>) {
array.forEach { println(it) }
}
使用示例:
val array = arrayOf(1, 2, 3)
printArray(array)
⚠️ 注意: 使用 *
时只能读取元素,不能写入,否则会编译错误。
5. 泛型约束(Generic Constraints)
泛型约束用于限制泛型参数的类型范围,最常见的是上界(upper bound)。
5.1. 单个上界
fun <T: Comparable<T>> sort(list: List<T>): List<T> {
return list.sorted()
}
使用示例:
val listOfInts = listOf(5, 2, 3, 4, 1)
val sorted = sort(listOfInts)
assertEquals(sorted, listOf(1, 2, 3, 4, 5))
5.2. 多个上界
如果一个泛型参数需要满足多个接口或类的约束,使用 where
:
fun <T> sort(xs: List<T>) where T : CharSequence, T : Comparable<T> {
// sort the collection in place
}
也可以用于类定义:
class StringCollection<T>(xs: List<T>) where T : CharSequence, T : Comparable<T> {
// omitted
}
✅ 提示: 每个 where
子句对应一个上界,可以有多个。
6. 泛型在运行时的表现
6.1. 类型擦除(Type Erasure)
和 Java 一样,Kotlin 的泛型在运行时会被擦除。例如:
val books: Set<String> = setOf("1984", "Brave new world")
val primes: Set<Int> = setOf(2, 3, 11)
在运行时,这两个变量都只是 Set
类型,无法区分其泛型类型。
✅ 提示: 编译器在编译时已经做了类型检查和自动转换,所以我们在使用时仍然可以安全访问。
6.2. 实化类型参数(Reified Type Parameters)
Kotlin 提供了 reified
关键字配合 inline
函数,使我们可以在运行时获取泛型类型信息。
例如,实现一个按类型过滤集合元素的函数:
inline fun <reified T> Iterable<*>.filterIsInstance() = filter { it is T }
使用示例:
val set = setOf("1984", 2, 3, "Brave new world", 11)
println(set.filterIsInstance<Int>()) // 输出 [2, 3, 11]
另一个实用示例是简化日志初始化:
inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)
class User {
private val log = logger<User>()
}
✅ 提示: reified
只能用于 inline
函数,因为内联函数的泛型会在调用点展开,编译器可以获取实际类型。
6.3. 深入理解 inline
与 reified
reified
的原理是:每次调用 inline
函数时,编译器会将函数体复制到调用点,并将泛型替换为实际类型。
例如:
class User {
private val log = logger<User>()
}
在编译时,logger<User>()
会被替换为:
LoggerFactory.getLogger(User::class.java)
因此,我们可以安全地在运行时获取泛型类型。
7. 总结
Kotlin 的泛型机制在保留 Java 泛型特性的同时,通过 out
、in
、reified
等关键字增强了类型安全和表达能力。掌握这些机制,有助于写出更安全、更简洁、更具表现力的代码。
本文所有代码示例均可在 GitHub 示例项目 中找到,建议结合实际项目练习使用。