1. 概述

Kotlin 的泛型与 Java 类似,但 Kotlin 在设计时做了更直观的改进,引入了 outin 等关键字,使泛型的使用更清晰易懂。

泛型在实际开发中非常常见,比如集合类、工具类、扩展函数等都需要泛型支持。本文将从参数化类、类型投影、泛型约束、运行时类型擦除等多个角度,带你深入了解 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 中的 outin 关键字

Kotlin 泛型中两个核心关键字是 outin,它们用于声明类型参数的协变(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. 深入理解 inlinereified

reified 的原理是:每次调用 inline 函数时,编译器会将函数体复制到调用点,并将泛型替换为实际类型。

例如:

class User {
    private val log = logger<User>()
}

在编译时,logger<User>() 会被替换为:

LoggerFactory.getLogger(User::class.java)

因此,我们可以安全地在运行时获取泛型类型。


7. 总结

Kotlin 的泛型机制在保留 Java 泛型特性的同时,通过 outinreified 等关键字增强了类型安全和表达能力。掌握这些机制,有助于写出更安全、更简洁、更具表现力的代码。

本文所有代码示例均可在 GitHub 示例项目 中找到,建议结合实际项目练习使用。


原始标题:Generics in Kotlin