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

发布时间:2026/11/2 1:49:06

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这类泛型组件应用到自己的项目中体会类型安全带来的收益。
延伸阅读

更多相关文章

2026/11/2 1:44:06

十分钟从零跑通 Hydra Launcher:下载+云存档

十分钟从零跑通 Hydra Launcher:下载云存档 【免费下载链接】hydra Hydra Launcher is an open-source gaming platform created to be the single tool that you need 项目地址: https://gitcode.com/GitHub_Trending/hy/hydra Hydra Launcher 是一款开源游…

2026/11/2 2:54:36

线性动态规划核心原理与工程实践指南

1. 线性动态规划基础概念线性动态规划(Linear Dynamic Programming)是动态规划中最基础也最常用的形式之一。这类问题的特点是状态转移方程呈现出明显的线性结构,通常表现为状态之间按照某种线性顺序进行转移。我在算法竞赛和实际工程问题中处…

2026/11/2 2:54:36

行政区划shp数据实战:清洗、投影、空间连接与统计避坑

行政区划shp数据这几个字看着平平无奇,但凡做过区域类项目的人都清楚,它几乎是最容易拿到、也最容易用歪的一份基础数据。下载下来解压,一堆同名不同后缀的文件摊在文件夹里,拖进桌面软件能正常显示,颜色一配、图例一挂…

2026/11/2 2:54:36

公有云GNS3缺失KVM硬件加速:嵌套虚拟化定位与TCG降级部署

GNS3 跑在云主机上,第一晚就栽在了 KVM 上。本地 VMware 里跑得好好的锐捷交换机镜像,挪到阿里云 ECS 上开机就卡在加速器检测环节,QEMU 进程反复退出,GNS3 控制台里只剩一行Could not access KVM kernel module: No such file or…

2026/11/2 2:54:36

CentOS 8安装第二个JDK:多版本共存与四层隔离实战

一台机器上同时挂着两套 Java 应用,一套跑在老框架上动不了,另一套又必须用新版本的语言特性,这种局面在 CentOS 8 上太常见了。标题里说的“安装第二个 JDK 隔离环境”,本质就是解决这件事:在系统自带的那份 OpenJDK …

2026/11/2 2:54:35

生活化类比理解五大经典算法原理

1. 算法学习新视角:用生活化类比理解核心原理每次打开算法教材看到满页的数学符号和复杂推导,你是不是也瞬间头大?作为从机械转码的过来人,我完全理解这种痛苦。直到有天我把排序算法想象成整理衣柜,把递归看作俄罗斯套…

2026/11/2 2:49:35

SAP Smart Forms 中文打印重叠与31字符压缩排查指南

简介:针对 SAP Smart Forms 在英文登录环境下中文内容显示重叠的实战排错文档,面向需要处理报表打印异常、Unicode 打印预览问题的 SAP 开发与运维人员。文档从现象切入,说明登录语言为 E 时,中文业务数据在打印预览中被挤压重叠、…

2026/11/1 0:01:26

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/1 1:01:33

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/1 0:01:26

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/1 1:01:33

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑