Kotlin泛型的协变与逆变

news2024/11/17 0:30:53

以下内容摘自郭霖《第一行代码》第三版

泛型的协变

一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置。

在这里插入图片描述

先定义三个类:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

定义一个SimpleData类

class SimpleData<T> {
    private var data: T? = null
    fun set(t: T?) {
    	data = t
    }
    fun get(): T? {
    	return data
    }
}

那么以下代码就会存在编译问题:

fun main() {
    val student = Student("Tom", 19)
    val data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data) 							// 实际上这行代码会报错
    val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
    val teacher = Teacher("Jack", 35)
    data.set(teacher)
}

即使Student是Person的子类,SimpleData<Student>并不是SimpleData<Person>的子类。

问题发生的主要原因是我们在handleSimpleData()方法中向SimpleData<Person>里设置了一个Teacher的实例。如果SimpleData在泛型T上是只读的话,肯定就没有类型转换的安全隐患了。

泛型协变的定义:假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。

要实现一个泛型类在其泛型类型的数据上是只读,则需要让MyClass<T>类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上。

修改SimpleData类的代码:

class SimpleData<out T>(val data: T?) {
    fun get(): T? {
    	return data
    }
}

在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。

由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data参数赋值了,所以这里改成了使用构造函数的方式来赋值。由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。

fun main() {
    val student = Student("Tom", 19)
    val data = SimpleData<Student>(student)
    handleMyData(data)
    val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {
	val personData = data.get()
}

由于SimpleData类已经进行了协变声明,那么SimpleData<Student>自然就是SimpleData<Person>的子类了,所以这里可以安全地向handleMyData()方法中传递参数。

然后在handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是完全安全的,所以这段代码没有任何问题。

Kotlin已经默认给许多内置的API加上了协变声明,其中就包括了各种集合的类与接口。

List简化版源码:

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    public operator fun get(index: Int): E
}

原则上在声明了协变之后,泛型E就只能出现在out位置上,可是在contains()方法中,泛型E仍然出现在了in位置上。这么写本身是不合法的,因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是contains()方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置上了。

泛型的逆变

假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。

逆变与协变的区别:

在这里插入图片描述

先定义一个Transformer接口,用于执行一些转换操作:

interface Transformer<T> {
	fun transform(t: T): String
}

现在尝试对Transformer接口进行实现:

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom", 19)
    val result = trans.transform(student)
}

这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用Transformer<Person>的匿名类实现将Student对象转换成一个字符串也是绝对安全的,并不存在类型转换的安全隐患。但是实际上,在调用handleTransformer()方法的时候却会提示语法错误,原因也很简单,Transformer<Person>并不是Transformer<Student>的子类型。

那么这个时候逆变就可以派上用场了,它就是专门用于处理这种情况的。修改Transformer接口中的代码:

interface Transformer<in T> {
	fun transform(t: T): String
}

这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。

Kotlin在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进去了。只要严格按照其语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance注解可以打破这一语法规则,但同时也会带来额外的风险。

逆变比较典型的例子就是Comparable的使用。Comparable是一个用于比较两个对象大小的接口,其源码定义如下:

interface Comparable<in T> {
	operator fun compareTo(other: T): Int
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/814632.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《golang设计模式》第一部分·创建型模式-03-建造者模式(Builder)

文章目录 1. 概念1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.3 类图 1. 概念 1.1 角色 Builder&#xff08;抽象建造者&#xff09;&#xff1a;给出一个抽象接口&#xff0c;以规范产品对象的各个组成成分的建造。ConcreteBuilder&#xff08;具体建造者&#xff09;&a…

移动机器人和UGV的自主导航(Matlab代码Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Redis - 底层数据结构

简介 Redis 的底层数据结构主要以下几种&#xff1a; SDS(Simple Dynamic String, 简单动态字符串)ZipList(压缩列表)QuickList(快表)Dict(字典)IntSet(整数集合)ZSkipList(跳跃表) 简单动态字符串 在 Redis 中&#xff0c;并不会直接使用 C 语言自带的字符串结构作为实际的…

hvv 云安全专项检测工具

过去的一年里&#xff0c;我们可能已经注意到了一个明显的趋势&#xff1a;安全对抗正逐步迁移至云端。 如&#xff1a;今年hw的要求中增加了云资产相关的得分项计算 如&#xff1a;越来越多的安全工具开始专注于云安全检测方向 如&#xff1a;越来越多的安全峰会加入了云原…

“一种蒸发冷凝水去除氨氮装置”技术专利

蒸发器中随着母盐液的蒸浓&#xff0c;溶液温度升高&#xff0c;由于氨氮易挥发的特性&#xff0c;游离氨挥发于水蒸气中&#xff0c;从而导致冷凝水中氨氮浓度上升&#xff0c;而影响出水水质。 “一种蒸发冷凝水去除氨氮装置”专利(证书号&#xff1a;第19422309号;专利号&am…

【前端】javascript+html+css 家具销售网站(代码+报告)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

移动端个人中心UI设计

效果图 源码如下 页面设计 <template><div class"container"><!-- 顶部用户信息 start--><div class"header"><div class"user-info"><van-image class"user-img" round width"70" :sr…

【打造超酷的GitHub主页】

文章目录 Github状态信息-api账户信息统计最常用语言Repo卡片 社交统计统计访问次数徽标 首先上地址&#xff1a;https://gitee.com/java_wxid/giteeprofile/blob/master/README.md 为了照顾一部分网络较差的的朋友们&#xff0c;这里使用国内的gitee仓库将主页代码提供给大家&…

C语言 位运算符 “|“ 的5种高级用法

前言 在上一篇文章中&#xff0c;我们介绍了&运算符的高级用法&#xff0c;本篇文章&#xff0c;我们将介绍| 运算符的一些高级用法。 一、人物简介 第一位闪亮登场&#xff0c;有请今后会一直教我们C语言的老师 —— 自在。 第二位上场的是和我们一起学习的小白程序猿 —…

16、博客列表加载效果 - 博客界面改造文章(202307)

最近闲来无事&#xff0c;就想着把博客界面弄得再有点动画效果&#xff0c;于是就找了延迟函数&#xff0c;把博客的内容列表加载的动画写出来了。 该动画效果还是挺炫的&#xff0c;但是因为刷新页面&#xff0c;下面列表的显示需要拉动到底下才看到&#xff0c;所以这里只能记…

微信小程序监测版本更新

在index.js里面 不放到app.js里面是因为有登录页面&#xff0c;在登录页面显示更新不太友好 onShow() {const updateManager wx.getUpdateManager()// 请求完新版本信息的回调updateManager.onCheckForUpdate(res > {if (res.hasUpdate) {// 新版本下载成功updateManage…

【腾讯云 Cloud Studio 实战训练营】体验搭建软件系统,无经验也能做开发

文章目录 前言IDE 解放开发者的生产力功能强大的 IDE快速搭建 Vue 开发环境注册 Cloud Studio进入Vue预置开发环境安装相关依赖包主文件 main.js 引入相关库和包首页增加移动端默认样式增加主要代码 IDE 的适用场景总结 前言 云计算技术的不断发展为代码开发带来了全新的体验&…

【Uniapp】支付链转二维码

前言 提示&#xff1a;这个是一个很小的项目&#xff0c;大概30分钟就能搞定 实现方式&#xff1a;输入支付代码&#xff0c;存储到对应的数据库表中&#xff0c;二维码访问一个PHP文件通过id来进行重定向&#xff0c;这样就可以使每张二维码都是固定的&#xff0c;替换二维码…

CS5213 国产HDMI转VGA带音频方案芯片|CS5213规格书|CS5213原理图

集睿致远/ASL推出的CS5213芯片是一个国产HDMI&#xff08;高清多媒体接口&#xff09;到VGA桥接芯片。它将HDMI信号转换为标准VGA信号它可以在适配器、智能电缆等设备中设计 CS5213特征 将HDMI信号转换为VGA输出 支持数字信号到模似信号的转换 支持 HDCP 1.0/1.1/1.2 操作简…

OpenAI API访问速度不佳?试试用Vercel来加速!

前言 众所周知&#xff0c;使用openAI API在国内访问体验并不佳&#xff0c;经常遇到访问较慢或者访问失败的问题。本文着重讲讲怎么解决这个问题&#xff0c;让我们日常开发和使用能够更方便的体验到AI带来的便利 为了帮大家省钱&#xff0c;也为了方便操作&#xff0c;实现…

论文解读|进一步融合:体积融合中6D姿态估计的多对象推理

原创 | 文 BFT机器人 01 背景 机器人等智能设备需要从它们的车载视觉系统中获得高效的基于物体的场景表示&#xff0c;以解释接触、物理和遮挡。已识别的精确对象模型将与未识别结构的非参数重建一起发挥重要作用。 本文提出了一个系统用于估计实时的接触和遮挡的精确姿态。从…

手写openFeign

目录 背景步骤应用级别&#xff1a;1、建立被调用方2、建立调用方引入依赖 3、实现效果 原理级别&#xff1a;调动方代码&#xff08;注意impl在启动的时候里边是空的&#xff09;引入jar包的代码&#xff08;写好以后推到nexus上&#xff0c;再拉到上述调用方服务中&#xff0…

收集用户隐私行为规范与修改指引

为更好地保护用户隐私信息&#xff0c;优化用户体验&#xff0c;平台对小程序内的收集用户隐私行为进行规范&#xff0c;开发者可自查所涉及的违规内容&#xff0c;并参照修改指引进行相应整改。 常见违规内容 一、隐私政策协议默示同意 小程序在收集用户数据前&#xff0c;…

【高级程序设计语言C++】二叉搜索树

1. 二叉搜索树的概念2. 二叉搜索树的功能2.1. 二叉搜索树的简单模型2.2. 二叉搜索树的查找2.3. 二叉搜索树的插入2.4. 二叉搜索树的删除 3. 二叉搜索树的性能分析 1. 二叉搜索树的概念 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是一种常见的二…

【C/C++】类之间的纵向关系——继承的概念

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…