Kotlin之泛型的高级特性

news2024/12/26 20:50:57

Kotlin泛型中的基本用法和Java中的泛型用法是大致相同的,因此也比较好理解。然而实际上,Kotlin在泛型方面还提供了不少特有的功能,接下来将进行介绍。

一、对泛型进行实化

泛型实化这个功能对于绝大多数Java程序员来讲是非常陌生的,因为Java中完全没有这个概念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java的泛型擦除机制才行。
Java的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK1.5之前的机制来运行,JVM是识别不出我们在代码中指定的泛型类型的。例如,假设我们创建一个List< String >集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行事情JVM并不知道它本来只打算包含哪张类型的元素,只能识别出来它是个List。

所有基于JVM的语言,他们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。
然而不同的是,Kotlin提供了一个内联函数的概念。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数的泛型声明。其工作原理如图:
在这里插入图片描述
内联函数的代码替换过程
最终代码会被替换成如图所示的样子:
)

可以看到 ,bar()是一个带有泛型类型的内联函数,foo()函数调用了bar()函数,在代码编译之后,bar()函数中的代码将可以获得泛型的实际类型。
这就意味着,Kotlin中是可以将内联函数中的泛型进行实化的。
那么具体该怎么写才能将泛型实化呢?首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化
示例代码如下:

inline fun <reified T> getGenericType(){
}

上述函数中的泛型T就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前提条件。接下来准备一个获取泛型实际类型的功能,代码如下所示:

inline fun <reified T> getGenericType()=T::class.java

这里实现了一个Java中完全不可能实现的功能:getGenericType()函数直接返回了当前指定泛型的实际类型。T.class这样的语法在Java中是不合法的,而在Kotlin中,借助泛型实化功能就可以使用T::class.java这样的语法了
现在我们就可以使用如下代码对getGenericType()函数进行测试:

fun main(){
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result1 is $result2")
}

这里给getGenericType()函数指定了两种不同的泛型,由于getGenericType()函数会将指定泛型的具体类型返回,因此这里我们将返回的结果进行打印。
在这里插入图片描述
可以看到,如果泛型指定成了Int,就可以得到java.lang.Integer的类型,如果将泛型指定成了String,那么就可以得到java.lang.String的类型。

二、泛型实化的应用

泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java这样的语法成为了可能。比如启动一个Activity就可以这么写:

val intent=Intent(context,TestActivity::class.java)
context.startActivity(intent)

对于TestActivity::class.java这样的语法可以通过Kotlin的泛型实化功能使得我们有更好的选择。
新建一个reified.kt文件,在里面编写如下代码:

inline fun <reified T> startActivity(context:Context){
    val intent=Intent(context,T::class.java)
    context.startActivity(intent)
}

这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来Intent接收的第二个参数本来应该是一个具体的Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用context的startActivity()方法来完成Activity的启动。
现在如果我们想要启动TestActivity,只需要这样写

 startActivity<TestActivity>(context)

Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。
不过现在的startActivity()函数其实还是有问题的,因为通常在启用Activity的时候还可能会使用Intent附带一些参数,比如下面的写法:

val intent=Intent(context,TestActivity::class.java)
intent.putExtra("param1","data")
intent.putExtra("param2",123)
context.startActivity(intent)

而经过刚才的封装之后,我们就无法进行传参了。
解决这个问题只需要利用高阶函数即可。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示:

inline fun <reified T> startActivity(context:Context,block:Intent.()->Unit){
    val intent=Intent(context,T::class.java)
    intent.block()
    context.startActivity(intent)
}

可以看到,这次的startActivity()函数中增加了一个函数类型参数,并且它的函数类型是定义在Intent类当中的。当创建完Intent的实例之后,随即调用该函数类型参数,并把Intent的实例传入,这样调用startActivity()函数的时候就可以在Lambda表达式中为Intent传递参数了,如下所示:

     startActivity<TestActivity>(context){
         putExtra("param1","data")
         putExtra("param2",124)
     }

三、泛型的协变

在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如图所示:
在这里插入图片描述
有了这个约定前提,我们就可以继续学习了。首先定义如下3个类:

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)

这里先定义了一个Person类,类中包含name和age这两个字段。然后又定义了Student和Teacher这两个类,让它们成为Person类的子类。
如果某个方法接收到一个List< Person >类型的参数,而我们传入一个List< Student >的实例,在Java中是不允许这么做的,因为List< String >不能成为List< Person >的子类,否则将可能存在类型转换的安全隐患
为什么会存在类型转换的安全隐患呢?下面我们通过一个具体的例子进行说明。这里自定义一个SimpleData类,代码如下所示:

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

SimpleData是一个泛型类,它的内部封装了一个泛型data字段,调用set()方法可以给data字段赋值,调用get()方法可以获取data字段的值。
接着我们假设,如果编程语言允许向某个接收SimpleData< Person >参数的方法传入SimpleData< Student >的实例,那么如下代码就会是合法的:

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)
}

在main()方法中,我们创建了一个Student的实例,并将它封装到SimpleData< Student >当中,然后将SimpleData< Student >作为参数传递给handleSimpleData()方法。但是handleSimpleData()方法接收的是一个SimpleData< Person >参数(这里假设可以编译通过),那么在handleSimpleData()方法中,我们就可以创建一个Teacher的实例,并用它来替换SimpleData< Person >参数中的原有数据。这种操作肯定是合法的,因为Teacher也是Person的子类,所以可以很安全地将Teacher的实例设置进去。
但是问题来了,回到main()方法中,我们调用SimpleData< Student >的get()方法来获取它内部封装的Student数据,可现在SimpleData< Student >中实际包含的却是一个Teacher的实例,那么必然产生类型转换异常。
所以为了杜绝这种安全隐患,Java中是不允许使用这种方式来传递参数的。换句话说,即使Student是Person的子类,SimpleData< Student >并不是SimpleData< Person >的子类。
问题的主要原因是我们再handleSimpleData()方法中向SimpleData< Person >里设置了一个Teacher的实例。如果SimpleData在泛型T上是只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData< Student >可不可以成为SimpleData< Person >的子类呢?
这里我们就引出了泛型协变的定义了。假设定义了一个MyClass< T >的泛型类,其中A是B的子类型,同时MyClass< A >又是MyClass< B >的子类型,那么我们就可以称MyClass在T这个泛型是协变的
但是如何才能让MyClass< A >成为MyClass< B >的子类型呢?如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass< T >类的所有方法都不能接收T类型参数。也就是说,T只能出现在out位置上,而不能出现在in位置上
现在修改SimpleData类的代码,如下所示:

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

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

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

}

由于SimpleData类已经进行了协变声明,那么SimpleData< Student >自然就是SimpleData< Person >的子类了,所以这里可以安全地向handleMyData()方法中传递参数
然后在handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是安全的。
Kotlin在许多内置的API加上了协变声明,其中就包括了各种集合的类与接口。Kotlin中的List本身就是只读的,如果你想要给List添加数据,需要使用MutableList才行。既然List是只读的,也就是说明他是可以协变的。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>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。原则上在声明了协变之后,泛型E就只能出现在out位置上,可是你会发现contains()方法中,泛型E仍然出现在了in位置上。这种写法本身是不合法的,因为in位置上出现了泛型E就意味着就有类型转换的安全隐患。但是contains()方法本身目的非常明确,他只是为了判断当前集合中是否包含参数传入的这个元素,并不会修改当前集合的内容,而且在泛型E的前面加上@UnsafeVariance 注解,这样编译器就会允许泛型E出现在in位置上了。

四、泛型的逆变

定义上,逆变和协变完全相反。假如定义了一个MyClass< T >的泛型类,其中A是B的子类型,同时MyClass< B >又是MyClass< A >的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图所示:
在这里插入图片描述
从直观上,逆变的规则有点奇怪,原本A是B的子类型,怎么MyClass< B >能反过来成为MyClass< A >的子类型呢?接下来我们通过一个例子举例:
先定义一个Transformer接口,用于执行一些转换操作,代码如下所示:

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

Transformer接口中声明了一个transform()方法,它接收一个T类型的参数,并且返回一个String类型的数据,这意味着参数T在经过transform()方法的转换后将会变成一个字符串。
我们对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)
    }

首先我们在main()方法中编写了一个Transformer< Person >的匿名类实现,并通过transform()方法将传入的Person对象转换成了一个“姓名+年龄”拼接的字符串。而handleTransformer()方法中创建了一个Student对象,并调用参数的transform()方法将Student对象转换成了一个字符串。
但是实际上在调用handleTransformer()方法的时候却会提示语法错误,原因很简单,Transformer< Person >并不是Transformer< Student >的子类型。
修改Transformer接口中的代码,如下所示:

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

这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。
这样我们修改了一下,此时Transformer< Person >已经成为了Transformer< Student >的子类型。
在Kotlin内置API的应用,比较典型的就是Comparable的使用。Comparable是一个用于比较两个对象大小的接口,其源码定义如下:

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

可以看到,Comparable在T这个泛型上就是逆变的,compareTo()方法则用于实现具体的比较逻辑。那么为什么要让Comparable接口是逆变的呢?如果我们使用Comparable< Person >实现了让两个Person对象比较大小的逻辑,那么用这段逻辑去比较两个Student对象的大小也成立,因此让Comparable< Person >成为Comparable< Student >的子类也合理。

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

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

相关文章

对云台、IMU、鲁棒性和硬件时间同步的理解

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> slam是一门集硬件和软件的多科学技术&#xff0c;涉及到很多技术术语、概念以及数学公式等等。下面我将结合网上资料以及个人理解进行介绍。 什么叫云台…

navicat导入sql数据库文件的简单操作步骤

目录 前言必读 一、概念 二、操作步骤 &#xff08;一&#xff09;新建连接 &#xff08;二&#xff09;新建数据库 &#xff08;三&#xff09;数据库导入sql文件 前言必读 读者手册&#xff08;必读&#xff09;_云边的快乐猫的博客-CSDN博客 一、概念 在很多项目当…

LeetCode 309. 最佳买卖股票时机含冷冻期

309. 最佳买卖股票时机含冷冻期 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;: 卖出股票后&#x…

第二个岳云鹏,跨年晚会含泪主持,成为一道最靓丽的风景

中国人自古讲究&#xff1a;百善孝入先&#xff0c;尤其是对于娱乐圈的明星来说&#xff0c;孝心和爱心更是他们成功的根本。 在这方面&#xff0c;德云社的小岳岳走在了前列&#xff0c;他用自己的孝心和爱心感动了粉丝&#xff0c;也收获了无数的鲜花和掌声。小岳岳的爱心体现…

2022年终总结 2023展望

一、回首2022 总结2022&#xff0c;同时也给新的一年设定目标&#xff0c;明年来还愿。 2022对我来说意义非凡&#xff0c;最重要莫过于考上了理想的研究生学校。 1~2月 考研初试刚结束&#xff0c;很长时间没敲过代码&#xff0c;而且本科期间刷的算法题不多&#xff0c;感觉…

02 USART

串口是一种应用十分广泛的通讯接口&#xff0c;串口成本低、容易使用、通信线路简单&#xff0c;可实现两个设备的互相通信。 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信&#xff0c;极大地扩展了单片机的应用范围&#xff0c;增强了单片…

RabbitMQ——高级特性

1、RabbitMQ高级特性 1.1、消息的可靠性投递 在使用 RabbitMQ 的时候&#xff0c;作为消息发送方希望杜绝任何消息丢失或者投递失败场景。 RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。 ⚫ confirm 确认模式 ⚫ return 退回模式 rabbitmq 整个消息投递的路径…

【回答问题】ChatGPT上线了!推荐30个以上比较好的命名实体识别模型

【回答问题】ChatGPT上线了&#xff01;推荐30个以上比较好的命名实体识别模型以及github源码&#xff1f; 推荐30个以上比较好的命名实体识别模型 命名实体识别模型是指识别文本中提到的特定的人名、地名、机构名等命名实体的模型。推荐的命名实体识别模型有&#xff1a; …

数据仓库搭建——本地数仓搭建详细流程

1 准备 1.1 本地虚拟机器准备 使用本地搭建三台机器&#xff0c;搭建数仓&#xff0c;模拟实际平台数据仓库的搭建。具体的搭建框架如下 安装软件角色主机1主机2主机3HadoopNameNode√SecondaryNameNode√DataNode√√√NodeManager√√√ResourceManager√√Zookeeperzk se…

educoder数据结构与算法 栈 第1关:实现一个顺序存储的栈

本文已收录于专栏 &#x1f332;《educoder数据结构与算法_大耳朵宋宋的博客-CSDN博客》&#x1f332; 目录 任务描述 相关知识 编程要求 测试说明 AC_Code 任务描述 本关任务是实现 step1/SeqStack.cpp 中的SS_IsFull、SS_IsEmpty、SS_Length、SS_Push和SS_Pop五个操作…

巨星大衣哥参加元旦跨年晚会,一首老歌赢得现场一片掌声

刚刚结束的跨年晚会&#xff0c;各地都展现出了极大热情&#xff0c;这是后疫情时代的第一次跨年&#xff0c;自由了的国人自然兴奋异常。从南国到北疆、从湖南到山东&#xff0c;人们都沉浸在喜悦当中&#xff0c;期盼祖国风调雨顺&#xff0c;期盼来年都能身体安康。 远在山东…

【回答问题】ChatGPT上线了!推荐30个以上比较好的中文bert系列的模型/压缩模型

推荐30个以上比较好的中文bert系列的模型 以下是一些中文 BERT 系列的模型: BERT-Base, Chinese: 中文 BERT 基础版 BERT-Large, Chinese: 中文 BERT 大型版 BERT-WWM, Chinese: 中文 BERT 加入了 whole word masking 的版本 BERT-WWM-Ext, Chinese: 中文 BERT 加入了 whole …

大数据NiFi(五):NiFi分布式安装

文章目录 NiFi分布式安装 一、​​​​​​​为什么需要NiFi集群 二、使用内嵌的zookeepe

字符串常量

文章目录1.内存分布&#xff08;1&#xff09; 代码区&#xff08;2&#xff09; 全局区&#xff08;3&#xff09; 栈区(stack)&#xff08;4&#xff09; 堆区(heap)2.字符串常量的存储字符数组与字符指针3.字符串常量内存释放问题4.字符串常量生命周期5.字符串常量定义6.字符…

Object类

文章目录面试题&#xff1a;和equals()的区别回顾&#xff1a;和equals重写后的调用重写equals方法的原则答案toString()方法面试题&#xff1a;和equals()的区别 回顾&#xff1a;和equals &#xff1a;运算符 1、可以使用在基本数据变量和引用数据变量中 2、如果比较的是基…

Model Animation动画页签

1、Animation动画页签 当我们选中包含动画剪辑的的模型时&#xff0c;该页签将显示动画设置相关的内容 动画剪辑是Unity动画的最小构成元素&#xff0c;代表一个单独的动作 当美术同学做好动画导出时建议将模型和动画文件分别导出 导出包含网格信息不包含动作信息模型导出不…

【大数据】CentOS7环境下安装MySQL数据库

文章目录1.安装mysql1.1 删除CentOS系统自带数据库1.2 开始安装mysql1.3 启动mysql服务&#xff0c;并设置密码1.4 授权所有外部用户连接MySQL&#xff08;了解&#xff09;2.客户端操作mysql常见查询操作解决汉字乱码问题创建操作1.安装mysql CentOS7自带的是Mariadb&#xf…

Prometheus学习和整理

是基于时序数据库的开源监控告警系统,非常适合对K8S集群的监控,它通过HTTP协议周期性的抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控, 特点: 支持多维数据模型&#xff1a;由度量名和键值对组成的时间序列数据 内置时间序列数据库TSDB 支持PromQL查…

AI修复照片

一、前言 最近闲来无事&#xff0c;翻了翻以前的老照片&#xff0c;看着多年前的老照片&#xff0c;感慨万千&#xff0c;仿佛又回到了以前的青春岁月。 只可惜青春易逝&#xff0c;无法重来。意气风发&#xff0c;头角峥嵘的画面只能永远地留存在相片之中了。只叹当时没有多拍…

Windows下数据资源公开访问之环境搭建方法(2023.1.2)

Windows下数据资源公开访问之环境搭建方法&#xff08;2023.1.2&#xff09;1、需求分析2、常用的解决方案2.0 前提环境&#xff08;Java Node&#xff09;2.1 方案一&#xff1a;利用现有服务器容器&#xff08;以Tomcat为例&#xff09;2.1.1 下载Tomcat并解压2.1.2 配置系统…