【Kotlin系列08】泛型进阶:从型变到具体化类型参数的类型安全之旅

引言:那个让我困惑三天的编译错误

还记得刚学Kotlin泛型时,我写了这样一段代码:

kotlin 复制代码
// 我以为这样可以...
val strings: List<String> = listOf("A", "B", "C")
val objects: List<Any> = strings  // ❌ 编译错误!Type mismatch

// 但这样却可以?
val readOnlyStrings: List<String> = listOf("A", "B")
val readOnlyObjects: List<Any> = readOnlyStrings  // ✅ 编译通过!

为什么第一个不行,第二个却可以? 这个问题困扰了我整整三天。后来才明白,这涉及到泛型的 型变(Variance) 概念。

再看这个场景:

kotlin 复制代码
// 想写一个通用的筛选函数
fun <T> filterItems(items: List<T>): List<T> {
    return items.filter { it is String }  // ❌ 怎么判断类型?
}

// 运行时类型擦除,泛型信息丢失了
fun <T> createInstance(): T {
    return T()  // ❌ 编译错误:Cannot use T as reified type parameter
}

这些问题的答案就在Kotlin泛型的高级特性中:型变、具体化类型参数、类型边界

今天,我们就来深入探索这些让泛型更强大、更安全的特性。

泛型基础回顾

什么是泛型

泛型允许我们编写可以处理多种类型的代码,同时保持类型安全:

kotlin 复制代码
// 没有泛型:需要为每种类型写一个类
class IntBox(val value: Int)
class StringBox(val value: String)
class UserBox(val value: User)

// 使用泛型:一个类搞定所有类型
class Box<T>(val value: T)

val intBox = Box(42)           // Box<Int>
val stringBox = Box("Hello")   // Box<String>
val userBox = Box(User())      // Box<User>

泛型的类型擦除

Java和Kotlin都使用类型擦除来实现泛型:

kotlin 复制代码
// 编译期
val stringList: List<String> = listOf("A", "B")
val intList: List<Int> = listOf(1, 2)

// 运行时:类型参数被擦除
// stringList和intList的运行时类型都是List,没有泛型参数信息

类型擦除的影响

kotlin 复制代码
// ❌ 不能在运行时检查泛型类型
fun <T> isListOf(value: Any): Boolean {
    return value is List<T>  // ❌ 编译错误:Cannot check for instance of erased type
}

// ❌ 不能创建泛型数组
fun <T> createArray(): Array<T> {
    return Array<T>(10) { }  // ❌ 编译错误
}

// ❌ 不能使用泛型类型的伴生对象
fun <T> getCompanion() {
    val companion = T.Companion  // ❌ 编译错误
}

型变(Variance):理解协变与逆变

为什么需要型变

先看一个问题:

kotlin 复制代码
// String是Any的子类型
val str: String = "Hello"
val any: Any = str  // ✅ 可以赋值

// 那么List<String>是List<Any>的子类型吗?
val stringList: List<String> = listOf("A", "B")
val anyList: List<Any> = stringList  // 这样可以吗?

答案取决于List型变设置。

不变(Invariant):默认行为

默认情况下,泛型类型是不变的

kotlin 复制代码
// MutableList是不变的
val stringList: MutableList<String> = mutableListOf("A", "B")
// val anyList: MutableList<Any> = stringList  // ❌ 编译错误

// 为什么不能?因为可能破坏类型安全:
val anyList: MutableList<Any> = stringList  // 假设允许
anyList.add(42)  // 加入一个Int
val first: String = stringList[0]  // 💥 运行时错误!


**不变性保证类型安全**:如果允许`MutableList`赋值给`MutableList`,就可能向字符串列表中添加非字符串元素,破坏类型安全。

协变(Covariant):out关键字

协变 意味着:如果AB的子类型,那么Producer<A>也是Producer<B>的子类型。

kotlin 复制代码
// List是协变的(只读)
interface List<out T> {  // out表示协变
    fun get(index: Int): T  // 只能读取(生产)T
    // fun add(element: T)  // ❌ 不能有接收T的方法
}

// 使用协变
val stringList: List<String> = listOf("A", "B")
val anyList: List<Any> = stringList  // ✅ 可以赋值
val item: Any = anyList[0]  // 只能读取,安全

协变的规则

  • 使用out关键字声明
  • 泛型类型只能出现在输出位置(返回值)
  • 不能出现在输入位置(参数)
kotlin 复制代码
// 协变类型示例
interface Producer<out T> {
    fun produce(): T  // ✅ 输出位置
    // fun consume(item: T)  // ❌ 输入位置,不允许
}

// 实际应用
class FruitBasket<out T : Fruit>(private val items: List<T>) {
    fun pick(): T = items.random()  // ✅ 只生产
    // fun add(fruit: T) { }  // ❌ 不能消费
}

val appleBasket: FruitBasket<Apple> = FruitBasket(listOf(Apple()))
val fruitBasket: FruitBasket<Fruit> = appleBasket  // ✅ 协变
val fruit: Fruit = fruitBasket.pick()  // 安全

逆变(Contravariant):in关键字

逆变 意味着:如果AB的子类型,那么Consumer<B>Consumer<A>的子类型(反过来)。

kotlin 复制代码
// Comparator是逆变的
interface Comparator<in T> {  // in表示逆变
    fun compare(a: T, b: T): Int  // 只能接收(消费)T
}

// 使用逆变
val anyComparator: Comparator<Any> = Comparator { a, b ->
    a.hashCode() - b.hashCode()
}
val stringComparator: Comparator<String> = anyComparator  // ✅ 可以赋值

// 为什么可以?
// 能比较Any的,当然也能比较String(String是Any的子类型)

逆变的规则

  • 使用in关键字声明
  • 泛型类型只能出现在输入位置(参数)
  • 不能出现在输出位置(返回值)
kotlin 复制代码
// 逆变类型示例
interface Consumer<in T> {
    fun consume(item: T)  // ✅ 输入位置
    // fun produce(): T  // ❌ 输出位置,不允许
}

// 实际应用
interface Sink<in T> {
    fun send(item: T)
}

class AnySink : Sink<Any> {
    override fun send(item: Any) {
        println("Received: $item")
    }
}

val anySink: Sink<Any> = AnySink()
val stringSink: Sink<String> = anySink  // ✅ 逆变
stringSink.send("Hello")  // 安全:Any的Sink可以接收String

型变总结

型变类型 关键字 类型关系 使用场景 示例
不变 无子类型关系 既读又写 MutableList<T>
协变 out 保持子类型关系 只读(生产者) List<out T>
逆变 in 反转子类型关系 只写(消费者) Comparator<in T>

**记忆口诀**: - **out** = 只**out**put(输出),生产者,保持方向 - **in** = 只**in**put(输入),消费者,反转方向

使用处型变(Use-site Variance)

有时我们不能修改类的声明,但想在使用时指定型变:

类型投影

kotlin 复制代码
// Array是不变的
val strings: Array<String> = arrayOf("A", "B")
// val objects: Array<Any> = strings  // ❌ 不变,不能赋值

// 使用out投影:只能读取
fun printArray(array: Array<out Any>) {  // 使用处协变
    for (item in array) {
        println(item)  // ✅ 可以读取
    }
    // array[0] = "X"  // ❌ 不能写入
}

printArray(strings)  // ✅ 可以传入Array<String>

// 使用in投影:只能写入
fun fillArray(array: Array<in String>, value: String) {  // 使用处逆变
    array[0] = value  // ✅ 可以写入
    // val item: String = array[0]  // ❌ 不能读取(返回Any?)
}

val objects: Array<Any> = arrayOf("X", "Y")
fillArray(objects, "Hello")  // ✅ 可以传入Array<Any>

星投影(Star Projection)

当你不关心具体类型时,可以使用星投影*

kotlin 复制代码
// 等价于out Any?
fun printList(list: List<*>) {  // List<*> ≈ List<out Any?>
    for (item in list) {
        println(item)  // item是Any?类型
    }
}

printList(listOf(1, 2, 3))
printList(listOf("A", "B"))

// MutableList<*>的限制
fun processArray(array: Array<*>) {
    val item: Any? = array[0]  // ✅ 可以读取为Any?
    // array[0] = "X"  // ❌ 不能写入
}

星投影的规则

  • Foo<*>Foo<out Any?>(如果是协变)
  • Foo<*>Foo<in Nothing>(如果是逆变)
  • Foo<*> = 既不能读也不能写(如果是不变)

具体化类型参数(Reified Type Parameters)

问题:类型擦除的限制

由于类型擦除,我们不能在运行时检查泛型类型:

kotlin 复制代码
// ❌ 不能检查泛型类型
fun <T> isInstance(value: Any): Boolean {
    return value is T  // ❌ 编译错误:Cannot check for instance of erased type
}

// ❌ 不能获取泛型的Class
fun <T> getClassName(): String {
    return T::class.simpleName  // ❌ 编译错误
}

解决方案:reified关键字

Kotlin的reified关键字配合inline函数,可以保留类型信息:

kotlin 复制代码
// ✅ 使用reified
inline fun <reified T> isInstance(value: Any): Boolean {
    return value is T  // ✅ 可以检查类型
}

// 使用示例
println(isInstance<String>("Hello"))  // true
println(isInstance<Int>("Hello"))     // false

// ✅ 获取Class对象
inline fun <reified T> getClassName(): String {
    return T::class.simpleName ?: "Unknown"
}

println(getClassName<String>())  // "String"
println(getClassName<List<Int>>())  // "List"

工作原理

kotlin 复制代码
// 调用代码
isInstance<String>("Hello")

// 编译器内联后
"Hello" is String  // 类型信息被保留

reified的实际应用

1. 类型安全的筛选
kotlin 复制代码
// 筛选特定类型的元素
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

val mixed: List<Any> = listOf(1, "two", 3, "four", 5.0)
val strings: List<String> = mixed.filterIsInstance<String>()
println(strings)  // [two, four]

val numbers: List<Int> = mixed.filterIsInstance<Int>()
println(numbers)  // [1, 3]
2. JSON反序列化
kotlin 复制代码
// Gson的类型安全包装
inline fun <reified T> Gson.fromJson(json: String): T {
    return fromJson(json, T::class.java)
}

val user: User = gson.fromJson<User>(jsonString)  // 类型安全
// 而不是:
// val user = gson.fromJson(jsonString, User::class.java) as User
3. 启动Activity(Android)
kotlin 复制代码
// Android中的类型安全Intent
inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

// 使用
startActivity<MainActivity>()  // 简洁且类型安全
4. 依赖注入
kotlin 复制代码
// 类型安全的依赖获取
inline fun <reified T> get(): T {
    return container.resolve(T::class.java)
}

val userService: UserService = get<UserService>()

**reified的限制**: - 只能用于`inline`函数 - 不能用于类的类型参数 - 不能用于属性 - 会增加生成的字节码大小(内联的代价)

泛型边界(Generic Bounds)

上界(Upper Bounds)

限制类型参数必须是某个类型的子类型:

kotlin 复制代码
// 单个上界
fun <T : Number> sum(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}

sum(1, 2)        // ✅ Int是Number的子类型
sum(1.5, 2.5)    // ✅ Double是Number的子类型
// sum("1", "2")  // ❌ String不是Number的子类型

// 实际应用:比较
fun <T : Comparable<T>> max(a: T, b: T): T {
    return if (a > b) a else b
}

println(max(10, 20))        // 20
println(max("apple", "banana"))  // banana

多个上界

有时需要同时满足多个约束:

kotlin 复制代码
// where子句指定多个上界
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable<T>,  // 必须可比较
          T : Cloneable {      // 必须可克隆
    return list.filter { it > threshold }.map { it.clone() as T }
}

// 实际应用
interface Named {
    val name: String
}

interface Identifiable {
    val id: String
}

fun <T> findById(items: List<T>, id: String): T?
    where T : Named,
          T : Identifiable {
    return items.find { it.id == id }
}

data class User(
    override val id: String,
    override val name: String
) : Named, Identifiable

val users = listOf(User("1", "Alice"), User("2", "Bob"))
val user = findById(users, "1")
println(user?.name)  // Alice

递归泛型边界

经典的Comparable模式:

kotlin 复制代码
// 自引用泛型边界
interface Comparable<T : Comparable<T>> {
    fun compareTo(other: T): Int
}

// 实现
class Person(val name: String, val age: Int) : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return age.compareTo(other.age)
    }
}

// 使用
fun <T : Comparable<T>> sortItems(items: List<T>): List<T> {
    return items.sorted()
}

val people = listOf(Person("Alice", 30), Person("Bob", 25))
val sorted = sortItems(people)  // 按年龄排序

实战案例:构建类型安全的Result容器

综合运用泛型的高级特性,构建一个完整的Result类型:

kotlin 复制代码
// 1. 基础定义:使用out协变
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// 2. 扩展函数:使用reified
inline fun <reified T> Result<*>.getOrNull(): T? {
    return when (this) {
        is Result.Success<*> -> data as? T
        else -> null
    }
}

// 3. map函数:协变保证类型安全
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> {
    return when (this) {
        is Result.Success -> Result.Success(transform(data))
        is Result.Error -> Result.Error(exception)
        is Result.Loading -> Result.Loading
    }
}

// 4. flatMap:处理嵌套Result
inline fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> {
    return when (this) {
        is Result.Success -> transform(data)
        is Result.Error -> Result.Error(exception)
        is Result.Loading -> Result.Loading
    }
}

// 5. 带边界的转换
inline fun <T : Any, R : Any> Result<T>.mapNotNull(
    transform: (T) -> R?
): Result<R> {
    return when (this) {
        is Result.Success -> {
            val transformed = transform(data)
            if (transformed != null) {
                Result.Success(transformed)
            } else {
                Result.Error(NullPointerException("Transform returned null"))
            }
        }
        is Result.Error -> Result.Error(exception)
        is Result.Loading -> Result.Loading
    }
}

// 6. 合并多个Result
fun <T> combineResults(results: List<Result<T>>): Result<List<T>> {
    val data = mutableListOf<T>()

    for (result in results) {
        when (result) {
            is Result.Success -> data.add(result.data)
            is Result.Error -> return result
            is Result.Loading -> return Result.Loading
        }
    }

    return Result.Success(data)
}

// 使用示例
class UserRepository {
    suspend fun loadUser(id: String): Result<User> {
        return try {
            val user = apiService.getUser(id)
            Result.Success(user)
        } catch (e: Exception) {
            Result.Error(e)
        }
    }

    suspend fun loadUserProfile(id: String): Result<UserProfile> {
        return loadUser(id)
            .map { user -> user.toProfile() }  // 类型安全转换
            .flatMap { profile ->  // 处理嵌套异步操作
                loadAdditionalData(profile.id).map { data ->
                    profile.copy(additionalInfo = data)
                }
            }
    }
}

// ViewModel中使用
class UserViewModel : ViewModel() {
    private val repository = UserRepository()

    val userState = MutableStateFlow<Result<UserProfile>>(Result.Loading)

    fun loadUserProfile(userId: String) {
        viewModelScope.launch {
            val result = repository.loadUserProfile(userId)

            // 使用reified扩展
            val profile: UserProfile? = result.getOrNull<UserProfile>()

            userState.value = result
        }
    }
}

这个案例展示了

  • out协变保证类型安全的转换
  • reified实现类型安全的值获取
  • ✅ 泛型函数实现灵活的操作符
  • ✅ 密封类结合泛型的强大表达能力

常见问题解答

Q1: 什么时候使用out,什么时候使用in?

A : 遵循PECS原则(Producer Extends, Consumer Super):

kotlin 复制代码
// Producer(生产者)使用out
interface Producer<out T> {
    fun produce(): T  // 只生产T
}

// Consumer(消费者)使用in
interface Consumer<in T> {
    fun consume(item: T)  // 只消费T
}

// 实际应用
fun copy(from: List<out Any>, to: MutableList<in String>) {
    // from是生产者,只读取
    // to是消费者,只写入
    for (item in from) {
        if (item is String) {
            to.add(item)
        }
    }
}

Q2: reified一定要配合inline使用吗?

A : 是的,reified必须用于inline函数:

kotlin 复制代码
// ✅ 正确:inline + reified
inline fun <reified T> check(value: Any): Boolean {
    return value is T
}

// ❌ 错误:不能单独使用reified
fun <reified T> check(value: Any): Boolean {  // 编译错误
    return value is T
}

// 原因:只有内联函数才能在编译时替换类型参数

为什么

  • inline函数在编译时会被展开到调用处
  • 编译器可以在展开时替换类型参数为具体类型
  • 非内联函数在运行时调用,类型信息已被擦除

Q3: 星投影什么时候使用?

A: 当你不关心具体类型,只需要访问与类型无关的成员时:

kotlin 复制代码
// 场景1:只需要知道是个List,不关心元素类型
fun printSize(list: List<*>) {
    println("Size: ${list.size}")  // size与类型无关
}

// 场景2:类型不确定但需要处理
fun processUnknown(value: Any) {
    when (value) {
        is List<*> -> println("List of ${value.size} items")
        is Map<*, *> -> println("Map of ${value.size} entries")
    }
}

// ❌ 不要这样:明确知道类型时不要用星投影
fun printStrings(list: List<*>) {  // 不好
    for (item in list) {
        println(item as String)  // 需要强制转换
    }
}

// ✅ 应该这样:明确类型
fun printStrings(list: List<String>) {  // 好
    for (item in list) {
        println(item)  // 类型安全
    }
}

Q4: 为什么Array是不变的,而List可以协变?

A : 因为可变性

kotlin 复制代码
// Array是可变的(可读可写)
val array: Array<String> = arrayOf("A", "B")
array[0] = "X"  // 可以修改

// 如果允许协变,会破坏类型安全:
// val objects: Array<Any> = array  // 假设允许
// objects[0] = 42  // 加入Int
// val str: String = array[0]  // 💥 运行时错误

// List是只读的(协变安全)
val list: List<String> = listOf("A", "B")
// list[0] = "X"  // ❌ 不能修改
val objects: List<Any> = list  // ✅ 安全,因为不能修改

规则

  • 只读集合List, Set, Map)可以协变
  • 可变集合MutableList, MutableSet, MutableMap)必须不变

练习题

练习1:实现泛型栈

kotlin 复制代码
// 实现一个类型安全的栈
class Stack<T> {
    private val items = mutableListOf<T>()

    fun push(item: T) { /* TODO */ }
    fun pop(): T? { /* TODO */ }
    fun peek(): T? { /* TODO */ }
    fun isEmpty(): Boolean { /* TODO */ }
}

// TODO: 添加协变的只读视图
// interface ReadOnlyStack<out T> { ... }

练习2:带边界的查找函数

kotlin 复制代码
// 实现一个通用的查找函数,要求:
// 1. T必须实现Comparable接口
// 2. T必须有name属性
// 3. 查找name匹配且值大于threshold的第一个元素

// TODO: 实现函数签名和函数体

练习3:使用reified简化反序列化

kotlin 复制代码
// 使用reified实现类型安全的JSON反序列化
class JsonParser {
    // TODO: 实现inline + reified函数
    // inline fun <reified T> parse(json: String): T { ... }
}

// 测试用例
data class User(val name: String, val age: Int)
val json = """{"name": "Alice", "age": 30}"""
val user: User = parser.parse<User>(json)  // 应该自动推断类型

总结

Kotlin的泛型系统在Java的基础上做了重要改进,提供了更强大和灵活的类型安全保障。

核心要点回顾

  1. 型变(Variance)

    • out(协变) - 生产者,保持子类型关系,只能输出
    • in(逆变) - 消费者,反转子类型关系,只能输入
    • 不变 - 默认行为,既可读又可写,无子类型关系
  2. 使用处型变

    • 类型投影:Array<out Any>, Array<in String>
    • 星投影:List<*>List<out Any?>
  3. 具体化类型参数(reified)

    • 配合inline函数使用
    • 保留运行时类型信息
    • 实现类型安全的类型检查和转换
  4. 泛型边界

    • 单个上界:<T : Number>
    • 多个上界:where T : A, T : B
    • 递归边界:<T : Comparable<T>>

最佳实践

  • 遵循PECS原则:Producer用out,Consumer用in
  • 优先使用声明处型变:在类定义时就指定out/in
  • 谨慎使用reified:只在确实需要运行时类型信息时使用
  • 明确泛型边界:使用上界约束类型,提高代码可读性
  • 避免过度泛型:不是所有代码都需要泛型化
  • 利用标准库:Kotlin标准库提供了丰富的泛型工具函数

泛型的设计哲学

Kotlin泛型体现了三个核心理念:

  1. 类型安全优先 - 在编译期尽可能捕获类型错误
  2. 灵活性与安全性平衡 - 通过型变提供灵活性,同时保证安全
  3. 实用主义 - reified等特性解决实际问题,而不拘泥于理论纯粹

正如文章开头的故事,理解泛型的这些高级特性,不仅能让我们写出更安全、更灵活的代码,更重要的是能帮助我们理解Kotlin类型系统的设计哲学。


相关资料

系列文章导航:


如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!

也欢迎访问我的个人主页发现更多宝藏资源

相关推荐
青衫客361 小时前
从应用到安全根:浅谈端侧系统能力、SA 与 REE / TEE 的技术体系
安全·操作系统
fareast_mzh1 小时前
Why Web2 → Web3 is slow
开发语言·web3
摘星编程2 小时前
React Native for OpenHarmony 实战:HorizontalScroll 横向滚动详解
android·react native·react.js
LiuPig刘皮哥2 小时前
llamaindex 使用火山embedding模型
windows·python·embedding
C++chaofan2 小时前
JUC并发编程:LockSupport.park() 与 unpark() 深度解析
java·开发语言·c++·性能优化·高并发·juc
灵犀坠2 小时前
Vue3 实现音乐播放器歌词功能:解析、匹配、滚动一站式教程
开发语言·前端·javascript·vue.js
行稳方能走远2 小时前
Android C++ 学习笔记5
android·c++
manok2 小时前
库博(CoBOT)vs 主流SAST工具:嵌入式高安全领域的差异化优势全景解析
安全·静态分析·代码审计·sast
shughui2 小时前
Android SDK 下载、安装与配置(详细图文附安装包,适配Appium+Python自动化)
android·python·appium·android-studio·android sdk