news 2026/10/7 20:41:11

Kotlin 泛型方差实战:out、in、星投影与类型擦除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kotlin 泛型方差实战:out、in、星投影与类型擦除

很多人记得“out是生产者,in是消费者”,但一遇到MutableList<Dog>、Array<Dog>或MutableList<*>就又要靠猜。方差真正解决的是一个安全问题:当子类型关系穿过泛型容器时,哪些赋值不会让我们读错或写错类型?

本文从最小反例开始,逐步讲清默认不变、协变、逆变、类型投影和 JVM 类型擦除。示例中的Dog、Cat都继承Animal。

一、为什么MutableList<Dog>不是MutableList<Animal>?

openclassAnimalclassDog:Animal()classCat:Animal()valdogs:MutableList<Dog>=mutableListOf(Dog())// val animals: MutableList<Animal> = dogs // 编译错误

假设最后一行能通过,调用方就可以执行animals.add(Cat()),而原来的dogs列表里会出现一只猫。这破坏了MutableList<Dog>的承诺。Kotlin 的MutableList<T>既读又写,因此对T不变。

对比只读接口:

valdogView:List<Dog>=dogsvalanimalView:List<Animal>=dogView// 可以:只从中读取 Animal

List<out E>在类型声明上协变,因为它不会通过这个接口接收一个新E。但 Kotlin 的只读接口不保证底层集合物理上不可变;其他持有dogs的代码仍可能改它。

二、out T:我只负责产出 T

interfaceProducer<outT>{funnext():T}valdogProducer:Producer<Dog>=object:Producer<Dog>{overridefunnext():Dog=Dog()}valanimalProducer:Producer<Animal>=dogProducervalanimal:Animal=animalProducer.next()

Dog是Animal的子类型,能产出Dog的对象当然也能被当成“产出Animal”使用。out限制类型参数主要出现在输出位置,换来Producer<Dog>可以安全赋给Producer<Animal>。

实际 API 中,读取缓存、查询只读数据和事件源都可能是“生产者”。如果一个类型还要接收T作为参数,就不能简单把整个类型标成out T;要重新审视接口职责,而不是用类型转换压掉编译器。

三、in T:我只负责消费 T

interfaceConsumer<inT>{funaccept(value:T)}valanimalConsumer:Consumer<Animal>=object:Consumer<Animal>{overridefunaccept(value:Animal){println(value)}}valdogConsumer:Consumer<Dog>=animalConsumer dogConsumer.accept(Dog())

一个能处理任意Animal的消费者,自然也能处理Dog。赋值方向和out正好相反:Consumer<Animal>可以当作Consumer<Dog>。排序比较器、回调参数接收器常体现这种思路。

可以把规则浓缩为:

声明主要允许的方向安全赋值示例
Producer<out T>向外给出TProducer<Dog>→Producer<Animal>
Consumer<in T>向内接收TConsumer<Animal>→Consumer<Dog>
MutableList<T>既读又写默认不能沿继承关系直接赋值

四、类本身不变时,用使用处类型投影

Array<T>可以读也可以写,因此它是不变的。有时某个函数只需要从数组读,或只需要向数组写,可以在使用这个类型的地方表达限制:

fun<T>copyItems(from:Array<outT>,to:Array<inT>){require(from.size<=to.size)for(indexinfrom.indices){to[index]=from[index]}}valfrom:Array<Dog>=arrayOf(Dog())valto:Array<Animal>=arrayOf(Animal())copyItems(from,to)

在这个函数里,from被承诺为只提供T,to被承诺为可接收T;因此不必把Array<Dog>整体伪装成Array<Animal>。类型投影是在限制当前视角,不是改变原对象的真实类型。

*星投影并非“完全没类型”

List<*>表示元素具体类型未知,但可以安全地把读出的元素当成Any?。对于MutableList<*>,你不知道它实际存的是Dog还是String,所以不能安全添加任意非空对象。星投影适合只做检查、遍历或转交的边界代码;进入业务层后,最好尽快恢复明确类型。

五、类型擦除与reified的边界

在 JVM 上,普通泛型类型参数大多会被擦除。下面的普通泛型函数不能直接判断value is T:

// fun <T> isValue(value: Any): Boolean = value is T // 编译错误inlinefun<reifiedT>isValue(value:Any):Boolean=valueisTprintln(isValue<Dog>(Dog()))// true

reified依赖内联,把实际类型参数信息带到调用点,因此可以写is T、T::class等。但它不是“所有泛型都完全保留到运行时”:例如List<String>的元素类型通常仍不能靠一次简单的运行时检查证明。要验证集合内容,仍需逐个检查元素或借助携带完整类型信息的序列化机制。

不要在不能内联的普通函数里强行通过as T声称“类型安全”。未经检查的强转只会把问题延后到运行时。

六、Android 项目里怎样用这些规则设计接口?

考虑 Repository 对外只暴露读取结果:

interfaceReadOnlyStore<outT>{suspendfunload():T}interfaceWriter<inT>{suspendfunsave(value:T)}

职责分开后,方差方向一眼就能看懂,也方便替换实现。若一个 Repository 同时load(): T和save(T),保持不变通常更诚实;不要为了让某个赋值编译通过就随意加out或@UnsafeVariance。

Java SDK 边界还可能带来原始类型与平台类型。读到List<*>时,不要立刻把它断言成List<User>;先解析或验证元素,再把明确的领域类型交给上层。泛型的目标是把错误尽量前移到编译期,而不是给强转换一套更复杂的写法。

七、面试高频问答

Q1:为什么List<Dog>可以赋给List<Animal>,MutableList<Dog>不行?
前者只读、声明为协变;后者可写,若允许赋值就可能把Cat写入狗列表。

Q2:out和in分别意味着什么?
out主要生产值,保留子类型到父类型的方向;in主要消费值,赋值方向相反。

Q3:Array<Dog>是Array<Animal>的子类型吗?
不是。Kotlin 的Array<T>既可读又可写,默认不变;可在函数参数处用Array<out T>/Array<in T>投影。

Q4:MutableList<*>能不能add(Dog())?
不能安全添加任意非空类型,因为实际元素类型未知。

Q5:reified是否让List<String>的每个元素都自动可验证?
不是。内联能让某些类型检查在调用点成立,但嵌套泛型的元素信息仍需要额外验证。

Q6:什么时候不该为了协变拆 API?
当对象本来就同时读写同一种T,维持不变更准确;设计应先表达真实能力,再考虑赋值便利。

记忆方差时,与其背“协变逆变”两个词,不如画一条数据流:数据从对象流出来,用out;数据流进对象,用in;双向流动,通常保持不变。

参考资料与延伸阅读

  • Kotlin 官方:Generics
  • Kotlin 官方:Inline functions
  • 本系列:Kotlin 入门与面试
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 20:40:46

基于Spring Boot和MySQL的路灯管理信息系统毕业设计实战

简介&#xff1a;一套面向高校毕业设计的Java Web路灯管理信息系统源码包&#xff0c;基于JSPServlet与MySQL数据库&#xff0c;采用B/S模式&#xff0c;可在MyEclipse或Eclipse中直接导入运行。压缩包共441个文件&#xff0c;约7.75MB&#xff0c;核心包含jsp动态页面、Java类…

作者头像 李华
网站建设 2026/10/7 20:40:41

Webpack太慢?Vite迁移实战:构建优化与配置指南

1. 为什么 Webpack 开发时那么慢&#xff1a;bundler 编译模型的瓶颈我之前接手过一个中后台项目&#xff0c;React TypeScript&#xff0c;页面模块大概 300 多个&#xff0c;node_modules里的依赖也不少。刚开始用 Webpack 4 开发&#xff0c;一次npm run dev冷启动要 28 秒…

作者头像 李华
网站建设 2026/10/7 20:40:14

上拉电阻与下拉电阻详解:作用、阻值计算与实战应用

1. 上拉、下拉电阻到底是什么做嵌入式硬件越久&#xff0c;越发现最基础的东西反而最容易被忽略。上拉电阻和下拉电阻看起来只是两个电阻&#xff0c;一个接到电源&#xff0c;一个接到地&#xff0c;但很多奇奇怪怪的问题&#xff0c;比如按键误触发、I2C通信死锁、MOS管误导通…

作者头像 李华
网站建设 2026/10/7 20:37:24

eBPF程序极限性能调优:规避512字节栈溢出与验证器分支爆炸实战

eBPF程序极限性能调优&#xff1a;规避512字节栈溢出与验证器分支爆炸实战编写运行在 Linux 内核态的 eBPF 字节码与编写常规用户态程序有着本质的范式差异。用户态程序只要语法正确&#xff0c;往往就能编译执行&#xff1b;而在 eBPF 的世界里&#xff0c;哪怕编译器输出了看…

作者头像 李华