泛型进阶:reified关键字、星投影、泛型约束与类型擦除、泛型在集合中的应用
泛型这东西,刚学Kotlin时觉得挺简单——不就是
一、类型擦除——JVM的“老毛病”
先说说类型擦除。Java的泛型是编译期玩的把戏,运行时类型信息就没了。Kotlin跑在JVM上,自然也继承了这一点。
核心事实:运行时你无法判断一个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
避坑指南:我曾经在写网络请求框架时,想用泛型区分成功回调和失败回调的类型,结果运行时全部擦除成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
六、知识体系总览
下面这张图把泛型进阶的核心知识点串起来了,方便你对照理解。
七、总结与建议
泛型进阶这块,说白了就是解决两个问题:编译期怎么保证类型安全,运行时怎么拿到类型信息。
- 类型擦除是JVM的固有特性,别想着绕过去,要学会接受它
- reified是Kotlin的杀手锏,写工具类时多用它
- 星投影适合处理异构集合,但要注意读写限制
- 泛型约束让泛型更精确,避免运行时类型转换
- 集合中的协变/逆变是Kotlin类型安全的重要保障
我的建议:刚开始接触泛型进阶时,别急着全用上。先从reified入手,它带来的体验提升最明显。等用顺手了,再慢慢理解协变逆变这些概念。我在项目里也是这么一步步来的。
公众号:蓝海资料掘金营,微信deep3321