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

发布时间:2026/11/2 1:49:01
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 Box<T> { private var value: T? = null fun set(v: T) { value = v } fun get(): T? = value } fun main() { val box = Box<String>() box.set("Kotlin") val s: String? = box.get() // 无需强转 println(s?.length) // box.set(123) // 编译期直接报错,类型安全 }

二、泛型类、泛型接口与泛型函数

2.1 泛型类

类型参数写在类名之后,可以在属性、方法参数和返回值中使用:

class Repository<T>(private val items: MutableList<T> = mutableListOf()) { fun add(item: T) { items.add(item) } fun all(): List<T> = items.toList() fun firstOrNull(): T? = items.firstOrNull() } fun main() { val repo = Repository<String>() repo.add("A") repo.add("B") println(repo.all()) // [A, B] println(repo.firstOrNull()) // A }

2.2 泛型接口

interface Converter<in I, out O> { fun convert(input: I): O } class StringToIntConverter : Converter<String, Int> { override fun convert(input: String): Int = input.trim().toInt() } fun main() { val converter: Converter<String, Int> = StringToIntConverter() println(converter.convert(" 42 ")) // 42 }

2.3 泛型函数

类型参数写在fun关键字之后、函数名之前,调用时通常可以由编译器自动推断:

fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null fun <T> printAll(vararg items: T) { items.forEach { println(it) } } fun <T : Comparable<T>> 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: List<T>): Double = list.sumOf { it.toDouble() } // 多重上界:使用 where 子句 fun <T> describe(value: T): String where T : CharSequence, T : Comparable<T> { 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")) // length=6, upper=KOTLIN }

注意:Kotlin 的泛型约束使用:而不是 Java 的extends,多重约束用where子句表达,可读性更好。

四、型变:out、in 与星投影

型变(Variance)描述的是「子类型关系能否传递到泛型类型上」。Kotlin 在声明处支持型变,这是它相比 Java 通配符更优雅的地方。

4.1 协变 out:只读生产者

out表示类型参数只出现在输出位置(返回值),因此Producer<String>可以赋值给Producer<Any>:

interface Producer<out T> { fun produce(): T // fun consume(item: T) // 编译错误:out 类型参数不能出现在入参位置 } class StringProducer : Producer<String> { override fun produce(): String = "hello" } fun main() { val p: Producer<Any> = StringProducer() // 协变:String 是 Any 的子类型 val value: Any = p.produce() println(value) }

4.2 逆变 in:只写消费者

in表示类型参数只出现在输入位置(参数),方向与协变相反:

interface Consumer<in T> { fun consume(item: T) } class AnyConsumer : Consumer<Any> { override fun consume(item: Any) = println("consumed: $item") } fun main() { val c: Consumer<String> = AnyConsumer() // 逆变:Consumer<Any> 可当作 Consumer<String> c.consume("kotlin") }

4.3 不变与星投影

既没有out也没有in的类型参数是不变的,例如MutableList<T>。当你不关心具体类型参数时,可以使用星投影*:

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: List<String>, dest: MutableList<in String>) { dest.addAll(src) } fun main() { val src = listOf("a", "b") val dest: MutableList<Any> = mutableListOf(1, 2) copyInto(src, dest) println(dest) // [1, 2, a, b] }

五、泛型与类型擦除

Kotlin 运行在 JVM 上,泛型信息在运行时会被擦除,因此不能直接写if (x is List<String>),也不能创建T()或Array<T>:

fun <T> create(): T { // return T() // 编译错误:类型擦除,无法实例化 throw UnsupportedOperationException() } fun check(list: List<*>) { // if (list is List<String>) { } // 编译错误:无法检查泛型实参 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(): List<T> { val result = mutableListOf<T>() 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".isInstanceOf<String>()) // true println(123.isInstanceOf<String>()) // false val mixed: List<Any> = listOf(1, "a", 2, "b") println(mixed.filterIsInstance<String>()) // [a, b] }

reified只能用于inline函数的类型参数,不能用于类或普通函数。它常用于filterIsInstance、startActivity<T>、JSON 反序列化等场景。

七、泛型与集合、扩展函数实战

7.1 手写一个类型安全的缓存

class Cache<K, V> { private val map = mutableMapOf<K, 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 = Cache<String, 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> List<T>.chunkedBySize(size: Int): List<List<T>> { 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 Result<out T> { data class Success<T>(val data: T) : Result<T>() data class Failure(val error: Throwable) : Result<Nothing>() } fun <T> runCatchingResult(block: () -> T): Result<T> = try { Result.Success(block()) } catch (e: Throwable) { Result.Failure(e) } fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = 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 }

这里Result<out T>使用协变,Failure继承Result<Nothing>,因为Nothing是所有类型的子类型,所以Result.Failure可以安全地作为任意Result<T>使用。

八、常见坑与最佳实践

  • 不要在需要写入的位置使用out:out类型参数不能作为方法参数类型,否则编译失败。
  • 区分List<T>与MutableList<T>:前者是协变的只读视图,后者是不变的可写集合。
  • 优先使用声明处型变:能通过out/in表达时,不要到处写使用处型变。
  • 谨慎使用reified:它会让函数内联,增大字节码体积,不适合体积很大的函数。
  • 类型擦除下避免依赖运行时泛型:需要类型判断时显式传入KClass或使用reified。
  • 用Nothing表达「无值」:在密封类结果类型中非常有用,可减少不必要的类型参数。

九、总结

Kotlin 泛型在保留 JVM 类型擦除现实的前提下,通过声明处型变、星投影、reified内联函数等特性,提供了比 Java 更简洁、更安全的表达方式。掌握以下三点即可覆盖大部分日常开发:

  1. 类型参数与约束:用上界和where精确描述类型能力。
  2. 型变规则:out用于生产者,in用于消费者,不变类型保持严格。
  3. 擦除与实化:理解擦除边界,在需要运行时类型时使用inline + reified。

建议把文中的示例在本地跑一遍,再尝试把Cache、Result这类泛型组件应用到自己的项目中,体会类型安全带来的收益。