泛型进阶:reified关键字、星投影、泛型约束与类型擦除、泛型在集合中的应用

泛型这东西,刚学Kotlin时觉得挺简单——不就是嘛。但用着用着就发现,坑不少。今天我把几个进阶知识点串起来讲,都是我在项目里踩过坑、后来才真正搞明白的东西。

一、类型擦除——JVM的“老毛病”

先说说类型擦除。Java的泛型是编译期玩的把戏,运行时类型信息就没了。Kotlin跑在JVM上,自然也继承了这一点。

核心事实:运行时你无法判断一个List和List有什么区别。

// 编译期没问题
val stringList: List<String> = listOf("a", "b")
val intList: List<Int> = listOf(1, 2)

// 但运行时,它们都是 List
// 下面这段代码在Java里会报错,Kotlin里也一样
// if (value is List<String>) { }  // 编译错误!

为什么会这样?因为JVM只认得List,不认得List。我刚开始做项目时,想写个通用解析器,根据泛型类型做不同处理,结果死活编译不过。后来才明白——类型擦除是JVM的底层限制,绕不过去。

避坑指南:我曾经在写网络请求框架时,想用泛型区分成功回调和失败回调的类型,结果运行时全部擦除成Object,导致类型转换异常。后来改用sealed class才解决。

二、reified关键字——让泛型“活”在运行时

Kotlin有个绝招——reified。它能让泛型在运行时保留类型信息。但有个前提:必须用在inline函数里。

// 普通泛型函数——运行时类型信息丢失
fun <T> checkType(value: Any): Boolean {
    // return value is T  // 编译错误!
    return false
}

// 使用reified——运行时保留类型
inline fun <reified T> checkType(value: Any): Boolean {
    return value is T
}

// 使用示例
fun main() {
    println(checkType<String>("hello"))  // true
    println(checkType<Int>("hello"))     // false
}

我个人习惯在写工具类时大量使用reified。比如解析JSON时,直接传类型参数,不用再传Class对象。

// 以前的做法
fun <T> parseJson(json: String, clazz: Class<T>): T {
    return mapper.readValue(json, clazz)
}

// 用reified的做法——清爽多了
inline fun <reified T> parseJson(json: String): T {
    return mapper.readValue(json, T::class.java)
}

// 调用时
val user = parseJson<User>(jsonString)

小提示:reified本质上是编译器在调用处把类型信息内联进去。所以inline函数体不能太长,否则会增加字节码体积。我一般控制在10行以内。

三、星投影——不知道类型时怎么办?

星投影用*表示,相当于Java的?通配符。但Kotlin的星投影更安全,它明确告诉你:我知道这里有类型,但我不确定具体是什么。

// 定义一个协变类型
interface Producer<out T> {
    fun produce(): T
}

// 星投影——只能读,不能写
fun processProducer(producer: Producer<*>) {
    val item: Any? = producer.produce()  // 可以读,返回Any?
    // producer.produce("hello")  // 编译错误!不能写
}

// 逆变类型的星投影
interface Consumer<in T> {
    fun consume(item: T)
}

fun processConsumer(consumer: Consumer<*>) {
    // val item = consumer.consume()  // 编译错误!不能读
    consumer.consume("hello")  // 可以写,但只能传Nothing类型
}

说白了,星投影就是“我知道这里有类型,但我不管它是什么”。我在项目中常用它来处理异构集合——比如一个列表里存了多种类型的Producer。

四、泛型约束——给类型加“门槛”

泛型不是万能的。有时候你需要限制T必须是某个类的子类,或者必须实现某个接口。这就是泛型约束的用武之地。

// 上界约束——T必须是Number的子类
fun <T : Number> sum(list: List<T>): Double {
    return list.sumOf { it.toDouble() }
}

// 多个约束——T必须同时实现Comparable和Serializable
fun <T> compareAndSerialize(a: T, b: T) where T : Comparable<T>, T : java.io.Serializable {
    println(a.compareTo(b))
}

// 可空类型的约束
fun <T : Any> nonNullOnly(value: T) {
    println(value.toString())
}

实战经验:我在写数据层时,经常用泛型约束来保证实体类必须实现序列化接口。这样序列化工具类就能统一处理,不用每个实体类单独写序列化逻辑。

五、泛型在集合中的应用

集合是泛型用得最多的地方。Kotlin的集合框架在设计时就充分考虑了泛型的协变和逆变。

// 协变——只读集合
val strings: List<String> = listOf("a", "b")
val anyList: List<Any> = strings  // 可以赋值,因为List是协变的

// 逆变——只写集合
val anyComparator: Comparator<Any> = Comparator { a, b -> 0 }
val stringComparator: Comparator<String> = anyComparator  // 可以赋值,因为Comparator是逆变的

// 不变——可变集合
val mutableStrings: MutableList<String> = mutableListOf("a", "b")
// val mutableAny: MutableList<Any> = mutableStrings  // 编译错误!MutableList是不变的

你想想看,为什么MutableList不能协变?因为如果允许,你就可以往MutableList里塞Int,运行时就会炸。Kotlin的设计很严谨——读用协变,写用逆变,读写都用就不变。

六、知识体系总览

下面这张图把泛型进阶的核心知识点串起来了,方便你对照理解。

泛型进阶 类型擦除 运行时类型信息丢失 JVM底层限制 reified关键字 运行时保留类型 必须配合inline 星投影 * 未知类型通配 协变/逆变安全 泛型约束 上界约束 : T : Number 多约束 where 集合中的应用 协变:List<out T> 逆变:Comparator<in T> 实战场景 JSON解析器 通用适配器 核心:编译期安全 + 运行时灵活 = 优雅的泛型代码

七、总结与建议

泛型进阶这块,说白了就是解决两个问题:编译期怎么保证类型安全,运行时怎么拿到类型信息。

  • 类型擦除是JVM的固有特性,别想着绕过去,要学会接受它
  • reified是Kotlin的杀手锏,写工具类时多用它
  • 星投影适合处理异构集合,但要注意读写限制
  • 泛型约束让泛型更精确,避免运行时类型转换
  • 集合中的协变/逆变是Kotlin类型安全的重要保障

我的建议:刚开始接触泛型进阶时,别急着全用上。先从reified入手,它带来的体验提升最明显。等用顺手了,再慢慢理解协变逆变这些概念。我在项目里也是这么一步步来的。


公众号:蓝海资料掘金营,微信deep3321