资讯详情

Kotlin 泛型详解:从类型参数到型变与实化类型

📅 2026/11/2 1:49:05 | 华诺云谱 👁 阅读
Kotlin 泛型详解:从类型参数到型变与实化类型
一、为什么需要泛型泛型Generics的核心价值是在编译期提供类型安全同时避免重复代码和强制类型转换。先看一个没有泛型的例子class BoxAny { private var value: Any? null fun set(v: Any?) { value v } fun get(): Any? value } fun main() { val box BoxAny() box.set(Kotlin) // 需要强制类型转换且转换失败只能在运行时暴露 val s: String box.get() as String println(s.length) }问题很明显Any丢失了具体类型信息错误只能在运行时发现。用泛型改写后类型信息被保留下来class BoxT { private var value: T? null fun set(v: T) { value v } fun get(): T? value } fun main() { val box BoxString() box.set(Kotlin) val s: String? box.get() // 无需强转 println(s?.length) // box.set(123) // 编译期直接报错类型安全 }二、泛型类、泛型接口与泛型函数2.1 泛型类类型参数写在类名之后可以在属性、方法参数和返回值中使用class RepositoryT(private val items: MutableListT mutableListOf()) { fun add(item: T) { items.add(item) } fun all(): ListT items.toList() fun firstOrNull(): T? items.firstOrNull() } fun main() { val repo RepositoryString() repo.add(A) repo.add(B) println(repo.all()) // [A, B] println(repo.firstOrNull()) // A }2.2 泛型接口interface Converterin I, out O { fun convert(input: I): O } class StringToIntConverter : ConverterString, Int { override fun convert(input: String): Int input.trim().toInt() } fun main() { val converter: ConverterString, Int StringToIntConverter() println(converter.convert( 42 )) // 42 }2.3 泛型函数类型参数写在fun关键字之后、函数名之前调用时通常可以由编译器自动推断fun T ListT.secondOrNull(): T? if (size 2) this[1] else null fun T printAll(vararg items: T) { items.forEach { println(it) } } fun T : ComparableT maxOf(a: T, b: T): T if (a b) a else b fun main() { println(listOf(1, 2, 3).secondOrNull()) // 2 printAll(a, b, c) println(maxOf(10, 20)) // 20 println(maxOf(apple, banana)) // banana }三、泛型约束上界与多重约束默认上界是Any?。通过上界可以限制类型参数的能力范围从而直接调用上界类型的方法// 单一上界 fun T : Number sumOfList(list: ListT): Double list.sumOf { it.toDouble() } // 多重上界使用 where 子句 fun T describe(value: T): String where T : CharSequence, T : ComparableT { return length${value.length}, upper${value.uppercase()} } fun main() { println(sumOfList(listOf(1, 2, 3))) // 6.0 println(sumOfList(listOf(1.5, 2.5))) // 4.0 println(describe(kotlin)) // length6, upperKOTLIN }注意Kotlin 的泛型约束使用:而不是 Java 的extends多重约束用where子句表达可读性更好。四、型变out、in 与星投影型变Variance描述的是「子类型关系能否传递到泛型类型上」。Kotlin 在声明处支持型变这是它相比 Java 通配符更优雅的地方。4.1 协变 out只读生产者out表示类型参数只出现在输出位置返回值因此ProducerString可以赋值给ProducerAnyinterface Producerout T { fun produce(): T // fun consume(item: T) // 编译错误out 类型参数不能出现在入参位置 } class StringProducer : ProducerString { override fun produce(): String hello } fun main() { val p: ProducerAny StringProducer() // 协变String 是 Any 的子类型 val value: Any p.produce() println(value) }4.2 逆变 in只写消费者in表示类型参数只出现在输入位置参数方向与协变相反interface Consumerin T { fun consume(item: T) } class AnyConsumer : ConsumerAny { override fun consume(item: Any) println(consumed: $item) } fun main() { val c: ConsumerString AnyConsumer() // 逆变ConsumerAny 可当作 ConsumerString c.consume(kotlin) }4.3 不变与星投影既没有out也没有in的类型参数是不变的例如MutableListT。当你不关心具体类型参数时可以使用星投影*fun printSize(list: List*) { println(size ${list.size}) // list[0] 的类型是 Any?只能安全读取 val first: Any? list.firstOrNull() println(first) } fun main() { printSize(listOf(1, 2, 3)) printSize(listOf(a, b)) }4.4 使用处型变如果类型来自 Java 或无法修改声明可以用out/in在使用处标注fun copyInto(src: ListString, dest: MutableListin String) { dest.addAll(src) } fun main() { val src listOf(a, b) val dest: MutableListAny mutableListOf(1, 2) copyInto(src, dest) println(dest) // [1, 2, a, b] }五、泛型与类型擦除Kotlin 运行在 JVM 上泛型信息在运行时会被擦除因此不能直接写if (x is ListString)也不能创建T()或ArrayTfun T create(): T { // return T() // 编译错误类型擦除无法实例化 throw UnsupportedOperationException() } fun check(list: List*) { // if (list is ListString) { } // 编译错误无法检查泛型实参 if (list is List*) { println(it is a list) } }如果需要保留类型信息可以显式传入KClass或使用reified见下一节。六、reified 与内联函数把泛型函数声明为inline并给类型参数加上reified编译器会在调用处内联展开从而在运行时拿到真实类型inline fun reified T Any?.isInstanceOf(): Boolean this is T inline fun reified T List*.filterIsInstance(): ListT { val result mutableListOfT() for (item in this) { if (item is T) result.add(item) } return result } inline fun reified T GsonLike.fromJson(json: String): T { // 伪代码真实场景可结合 Gson/Jackson 的 TypeToken Suppress(UNCHECKED_CAST) return json as T } fun main() { println(abc.isInstanceOfString()) // true println(123.isInstanceOfString()) // false val mixed: ListAny listOf(1, a, 2, b) println(mixed.filterIsInstanceString()) // [a, b] }reified只能用于inline函数的类型参数不能用于类或普通函数。它常用于filterIsInstance、startActivityT、JSON 反序列化等场景。七、泛型与集合、扩展函数实战7.1 手写一个类型安全的缓存class CacheK, V { private val map mutableMapOfK, V() fun put(key: K, value: V) { map[key] value } fun get(key: K): V? map[key] fun getOrPut(key: K, defaultValue: () - V): V map.getOrPut(key, defaultValue) } fun main() { val cache CacheString, Int() cache.put(a, 1) println(cache.get(a)) // 1 println(cache.getOrPut(b) { 2 }) // 2 println(cache.getOrPut(b) { 100 }) // 2已存在不会覆盖 }7.2 泛型扩展函数链式转换fun T, R T.letMap(transform: (T) - R): R transform(this) fun T ListT.chunkedBySize(size: Int): ListListT { require(size 0) { size must be positive } return (0 until this.size step size).map { subList(it, minOf(it size, this.size)) } } fun main() { val result 123.letMap { it.toInt() * 2 } println(result) // 246 println(listOf(1, 2, 3, 4, 5).chunkedBySize(2)) // [[1, 2], [3, 4], [5]] }7.3 泛型 密封类类型安全的结果封装sealed class Resultout T { data class SuccessT(val data: T) : ResultT() data class Failure(val error: Throwable) : ResultNothing() } fun T runCatchingResult(block: () - T): ResultT try { Result.Success(block()) } catch (e: Throwable) { Result.Failure(e) } fun T, R ResultT.map(transform: (T) - R): ResultR when (this) { is Result.Success - Result.Success(transform(data)) is Result.Failure - this } fun main() { val ok runCatchingResult { 10 / 2 }.map { it * 3 } val err runCatchingResult { 10 / 0 }.map { it * 3 } when (ok) { is Result.Success - println(ok ${ok.data}) // ok 15 is Result.Failure - println(error ${ok.error}) } println(err is Result.Failure) // true }这里Resultout T使用协变Failure继承ResultNothing因为Nothing是所有类型的子类型所以Result.Failure可以安全地作为任意ResultT使用。八、常见坑与最佳实践不要在需要写入的位置使用outout类型参数不能作为方法参数类型否则编译失败。区分ListT与MutableListT前者是协变的只读视图后者是不变的可写集合。优先使用声明处型变能通过out/in表达时不要到处写使用处型变。谨慎使用reified它会让函数内联增大字节码体积不适合体积很大的函数。类型擦除下避免依赖运行时泛型需要类型判断时显式传入KClass或使用reified。用Nothing表达「无值」在密封类结果类型中非常有用可减少不必要的类型参数。九、总结Kotlin 泛型在保留 JVM 类型擦除现实的前提下通过声明处型变、星投影、reified内联函数等特性提供了比 Java 更简洁、更安全的表达方式。掌握以下三点即可覆盖大部分日常开发类型参数与约束用上界和where精确描述类型能力。型变规则out用于生产者in用于消费者不变类型保持严格。擦除与实化理解擦除边界在需要运行时类型时使用inline reified。建议把文中的示例在本地跑一遍再尝试把Cache、Result这类泛型组件应用到自己的项目中体会类型安全带来的收益。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑