这回就好好聊聊Kotlin的泛型

news2024/11/19 15:37:02

公众号「稀有猿诉」        原文链接 这回就好好聊聊Kotlin的泛型

泛型(Generics)是静态强类型编程语言中非常强大的特性,可以极大的加强代码的复用,并增强类型安全,减少运行时的类型转换错误。在这篇文章就来详细的学习一下Kotlin中对泛型的支持情况,并学会写出类型安全的可复用代码。

泛型基础

泛型的本质就是能够创建参数化的对象和函数,以实现复用。比如说,我们最熟悉的集合List,它是与具体类型无关的数据结构,或者叫做对象容器。列表List的重点在于可扩展长度,但里面具体的对象类型并不是重点,只要是一个对象就可以了。假如没有泛型,可能就要写很多重复的代码,比如字符串列表StringList,数字列表NumberList,等等。用泛型,只用一个参数化的List就可以了,用尖括号**<>**来表示参数化。

val names: List<String> = listOf("James", "Kevin", "Harden")
val rebounds: List<Int> = listOf(2, 14, 7)

泛型有两种形式,一种是对类进行参数化如List,一种是对函数进行参数化,如max()。

参数化的类

声明方式就是在声明类的时候在类的名字后面用尖括号**<>**来带上一个类型参数,然后在内部就可以当成一个类型来使用:

class Box<T>(t: T) {
	var value = t
}

这就创建了一个参数化的容器,它可以持有任何指定类型的对象:

val box: Box<Int> = Box<Int>(1)
val case: Box<String> = Box<String>("Coat")

参数化的函数

除了参数化的类以外,还可以创建参数化的函数,在函数名字的前面用尖括号**<>**来声明泛型,然后在参数列表以及函数体内就可以当作类型来使用:

fun <T> singleTonList(item: T): List<T> {
	...
}

调用的时候指定一下具体的类型就可以了:

val l = singletonList<Int>(3)

**注意:**Kotlin语言有强大的类型推断能力,但凡编译器能够推断出类型时,类型的声明都可以省略掉。对于泛型更是如此,比如说,这样写都是合法的:

val names = listOf("James", "Kevin", "Harden")
val rebounds = listOf(2, 14, 7)
val l = singletonList(3)

通常情况下,声明定义赋值三个地方,只要有一个地方能够让编译器知道具体的类型就够了,其他地方都可以把类型的声明省略掉。

泛型的本质与优点

假如不使用泛型,又想写出比较通用的类和函数,唯一可行的方法就是使用通用基类Any当作参数,在Kotlin中Any是所有对象的基类,比如,说想实现一个列表:

class AnyList {
	fun add(item: Any)
	fun get(idx: Int): Any
}

这样写可以,但它有很大的问题,就是不能保证类型安全:

val list = AnyList()
list.add("James")
list.add(13)
val e = (Int) list.get(1)

一方面我们需要自己进行强行类型转换,但也无法保证你取出来的对象类型与期望的是一致的,更无法保证调用者往里面添加什么对象,因为任何Any的子类都可以让代码通过编译,但在运行时极容易发生类型转换异常ClassCastException。

但用泛型就能很好的解决这个问题,可以得出泛型的优点:

  1. 不需要做类型转换,编译器会根据指定的具体类型自动做类型转换
  2. 类型安全,编译器会帮助做检查,传给泛型的对象必须具有一致的类型,且是指定的类型
  3. 保障了运行时的类型安全,因为编译器在编译时做好了检查,所以不会发生运行时的类型错误

因此,凡是有需要针对 类型复用的地方,都应该用泛型来实现类型参数化。

关键字out和关键字in

大部分情况下,只要给类型和函数加上参数化的类型就够了,但有时候有些复杂情况需要处理。

协变与逆变

协变与逆变Covariance and Contravariance是用来描述具有父子继承关系的简单类型,在通过参数化的方法构造出来的更复杂的类型之间是否能保持父子关系的术语。

比如Dog是Animal的子类,根据继承和多态,Dog可以用在任何声明为Animal的语句和表达式中。变型Variance指的就是根据已知的父子关系Dog和Animal,如何来确定由它们构成的更复杂类型如List<Dog>和List<Animal>之间的关系?

常规泛型是不可变的Invariant,也就是说复杂类型之间的关系与它们具体的参数化类型之间是没有关系的,如List<Dog>并不是List<Animal>,它们之间没有任何关系,不能把List<Dog>当成是List<Animal>,虽然Dog可以被当作Animal。

不可变Invariant有时候会带来不方便,比如说,集合通常都有addAll方法来批量的把对象加入到集合中:

class List<T> {
	fun addAll(from: List<T>) {
		for (x in from) {
			add(x)
		}
	}
}
val objs: List<Any> = emptyList()
val names: List<String> = listOf("James", "Kevin", "Harden")
objs.addAll(names) // No go, compile error

这是参数化列表集合,先创建一个具体类型为Any的列表,然后尝试把一个String列表添加到Any列表中,其实这么做是完全安全的,因为String对象是完全可以当作其基类Any来使用的,但泛型的不可变性阻止了我们这么做。

这时就需要协变逆变了,也就是通过一定的方法让复杂类型的行为与其参数化类型之间进行协同。

关键字out进行协变

使用out关键能够让泛型进行协变。比如上面例子理想的情况应该是,只要能当作T的类型,都应该能用在addAll中,换句话说把T的子类的列表也应该能够支持,即objs.addAll(names)应该能正常编译并正常运行。使用关键out即可达到这样的效果:

class List<out T> {
	fun addAll(from: List<T>) {
		for (x in from) {
			add(x)
		}
	}
}
val objs: List<Any> = emptyList()
val names: List<String> = listOf("James", "Kevin", "Harden")
objs.addAll(names) // Okay

这里的泛型参数from: List其实是一个生产者,它生产类型为T的对象,所以这里用out来修饰,产出的对象是T或者是T的子类都是会是合法的。或者说当我们想把一个子类的泛型赋给父类的泛型时,就需要对泛型声明为out,以进行协变。

**注意:**关键字out与Java泛型中的extend通配符的作用是一样的,指定参数的上限,生产者产生的对象都会向上转型(upcast)为基类,所以需要指定一个上限。

与之相对的,还有in逆变。

关键字in进行逆变

有时候情况是相反的,也就是说我们持有的是父类的泛型,但 我们想把它赋给其子类的泛型,这时就可以用in进行逆变。而且必须注意in只能用在消费者中,也就是说是在真实消费对象,为什么呢?其实这里真实发生的是向下转型(downcast)–把父类的对象赋给子类的引用上面,而向下转型不一定保证是安全的。所以,必须是在真实消费这个对象的地方,只有是期望的真实对象才能被消费。

class ParameterizedConsumer<in T> {
    fun toString(value: T): String {
        return value.toString()
    }
}

val parameterizedConsumer = ParameterizedConsumer<Number>()

val ref: ParameterizedConsumer<Double> = parameterizedConsumer

**注意:**关键字in与Java泛型中的super是一样的,指定一个下限,因为在消费对象时会转成T,用T来限制成为下限,那么向下转型(downcast)就是安全的。

任意类型的泛型

有些比较简单粗暴的场景,就是单纯的想让任意类型的泛型都可以使用,这时关键字out和关键字in可能都不太合适,因为它们只能用于生产者和消费者场景,用以指定类型上限和类型下限。这时可以用**星号***来当用泛型参数,以表示任意具体类型的泛型都可以使用。

fun printArray(array: Array<*>) { 
    array.forEach { println(it) }
}

val array = arrayOf(1,2,3) 
printArray(array)

关键字reified

运行时泛型擦除

需要注意的是泛型类型在运行时会被擦除(erased),也就是说在运行时任何对象都是不带有其泛型类型的,具体点的,就是List<String>和List<Int>在运行时,它们的对象实例是一样的,无法知道它们的具体的泛型参数类型。前面讲的各种规则都是发生在编译时间,编译器帮助检查传入的泛型对象是否符合规划,并进行类型转换。到了运行时,泛型类型会被擦除。(为啥会被擦除呢?因为JVM要保持向后兼容,早期的Java没有泛型,只有原始的类型对象(raw type),所以后来1.5版本后加入的泛型只有擦除掉变成raw type才能保持兼容。)

关键字reified

泛型类型擦除会带来一个问题,就是对于泛型类型对象,无法做类型检查(is T),无法做类型转换(as T),因为运行时的对象根本不知道它的泛型类型是什么,这会带来极大的不方便,特别是工厂方法就无法使用泛型了,因为无法做类型检查 和转换。

这时inline再加上关键字reified就能完美的解决问题,它们两个配合起来运行时就能保留泛型类型了:

inline fun <reified T> Iterable<*>.filterIsInstance() = filter { it is T }

>> val set = setOf("1984", 2, 3, "Brave new world", 11)
>> println(set.filterIsInstance<Int>())
[2, 3, 11]

可以看到类型判断起来作用了。再看一个泛型工厂方法的例子:

inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)

class User {
    private val log = logger<User>()
    // ...
}

练习

这里强烈推荐谷歌官方给出的关于Kotlin语言中的类型相关的小练习,可以用来巩固加强一下所学的知识。

参考资料

  • Generics: in, out, where
  • Kotlin generics
  • Generics in Kotlin
  • Kotlin 泛型中的 in 和 out
  • Kotlin泛型<in, out, where>概念及示例
  • Kotlin 的泛型

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!

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

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

相关文章

080|为什么阿里的价值观值得你关注?

在阿里巴巴20周年年会现场&#xff0c;万众瞩目之下&#xff0c;马云和张勇完成了阿里巴巴董事长职务的交接。 不过你也知道&#xff0c;这次接棒在一年前就已经公布了&#xff0c;在年会上只是一个仪式。在20周年年会过后&#xff0c;我找到了互联网圈的资深媒体人阳淼&#…

Mathcad tips_table相关

1. 可以插入表格&#xff0c;或者2. 从excel 文件导入 选择列 选择其中一行的数值

408专业课130+|我的备考经验和复盘

408的四门课任务量多到爆炸&#xff01;但难度不止于此。别忘了大部分选计算机的勇士们&#xff0c;是要考数学的&#xff01;直接起飞。 408数学无疑是王炸王炸&#xff0c;要想上岸就一定要把这两个大头一起拿下&#xff01; 作为一个成功上岸的非计算机专业跨考生&#xf…

计算机网络实验一 网线制作

实验目的与要求&#xff1a; 实验目的 了解以太网网线&#xff08;双绞线&#xff09;和制作方法 实验内容 了解网线和水晶头 学习网线制作方法 实验环境和要求 网线 水晶头 压线钳 剥线钳 网线测试器 方法、步骤&#xff1a; 步骤一 准备工具和材料 步骤二 剥掉双绞线的外…

16 PyTorch 神经网络基础【李沐动手学深度学习v2】

要想直观地了解块是如何工作的&#xff0c;最简单的方法就是自己实现一个。 在实现我们自定义块之前&#xff0c;我们简要总结一下每个块必须提供的基本功能。 将输入数据作为其前向传播函数的参数。 通过前向传播函数来生成输出。请注意&#xff0c;输出的形状可能与输入的形…

leetcode 热题 100_找到字符串中所有字母异位词

题解一&#xff1a; 滑动窗口&#xff1a;类似于字符串匹配&#xff0c;但匹配异位词需要包含相同的字母及个数&#xff0c;可以分别用两个数组存储字符串s滑动窗口和字符串p的字母及个数&#xff0c;再用Array.equals()进行比对。对于s.length()<p.length()的情况需要特判。…

2024 全国水科技大会暨污泥处理处置与资源化利用技术论坛(九)

为大会征集“绿色低碳污水厂案例”&#xff0c;欢迎各相关单位积极报名&#xff01; 一、会议背景 为深入学习贯彻《中共中央、国务院关于全面推进美丽中国建设的意见》&#xff0c;全面贯彻实施《固体废物污染环境防治法》、《“十四五”全国城市基础设施建设规划》&#xff0…

【操作系统概念】 第2章:操作系统结构

文章目录 0. 前言2.1 操作系统的服务2.2 操作系统的用户界面2.3 系统调用&#xff08;System Call&#xff09;2.3.1 系统调用的过程 2.4 系统调用类型2.5 系统程序分类2.6 操作系统设计和实现2.7 操作系统结构2.7.1 简单结构2.7.2 分层方法2.7.3 微内核2.7.4 模块 2.8 操作系统…

IO多路复用:2024/3/5

作业1、使用poll实现tcp服务器端&#xff0c;select实现tcp客户端 服务器端&#xff1a; #include <myhead.h> #define SER_IP "192.168.199.131" //服务端IP #define SER_PORT 8888 //服务端端口号int main(int argc, const char *argv[])…

【大数据】通过 docker-compose 快速部署 MinIO 保姆级教程

文章目录 一、概述二、MinIO 与 Ceph 对比1&#xff09;架构设计对比2&#xff09;数据一致性对比3&#xff09;部署和管理对比4&#xff09;生态系统和兼容性对比 三、前期准备1&#xff09;部署 docker2&#xff09;部署 docker-compose 四、创建网络五、MinIO 编排部署1&…

第一节 JDBC是什么?

JDBC代表Java数据库连接(Java Database Connectivity)&#xff0c;它是用于Java编程语言和数据库之间的数据库无关连接的标准Java API&#xff0c;换句话说&#xff1a;JDBC是用于在Java语言编程中与数据库连接的API。 JDBC库包括通常与数据库使用相关&#xff0c;如下面提到的…

基于Vue3的在线考试系统

TDuckX 是一个功能强大的可私有化部署的在线表单考试平台&#xff0c;可以帮助您轻松创建表单和在线考试。本文档将指导您如何使用 TDuckX 创建您自己的在线考试。 步骤 1&#xff1a;登录账户 完成系统部署后&#xff0c;在浏览器中打开 TDuckX 的网站。 如果您已经拥有账户&…

docker单节点搭建在线商城

本文档使用到的软件包以上传到资源中 目录 1. 创建容器并配置基础内容 1.1 将gpmall-repo上传到容器中 1.2 添加yum源 2. 安装基础服务 2.1 安装JAVA环境 2.2 安装Redis缓存服务 2.3 安装Elasticsearch服务 2.4 安装Nginx服务 2.5 安装MariaDB数据库 2.6 安…

积分商城管理系统的设计与实现(含源文件)

项目源码&#xff1a;https://gitee.com/oklongmm/biye2 系统介绍&#xff1a; 积分商城管理系统&#xff0c;包括用户模块、商品模块、积分模块和后台管理模块。 一、用户模块&#xff1a; 用户注册与登录&#xff1a;用户可以创建账户并登录系统。 个人信息管理&#xff1…

android开发教程视频,android组件化和插件化

第一阶段&#xff1a;Android 基础知识回顾&#xff1a; 回顾Android 开发编程&#xff0c;深入理解Android系统原理和层次结构&#xff0c;深入分析Handler源码和原理&#xff1b;回顾Java&#xff0c;C/C&#xff0c;Kotlin、dart 在Android开发中必用的语言&#xff0c;熟悉…

四平方和 刷题笔记

/* 四平方和 直接暴力搜索 可能会超时 使用二分辅助搜索 先枚举出 c*cd*d并存入数组 用式子算出 a*ab*b还剩下多少查找sum数组里面是否存在符合条件的数 查找方式使用二分搜索 当逼近答案后 检查一下是否为所需的数 如果是 直接输出 */ #include <cstring> #includ…

分布式数字身份:通往Web3.0世界的个人钥匙

数字化时代&#xff0c;个人身份已不再仅仅局限于传统形式&#xff0c;分布式数字身份&#xff08;Decentralized Identity&#xff0c;简称DID&#xff09;正崭露头角&#xff0c;它允许个人通过数字签名等加密技术&#xff0c;完全掌握和控制自己的身份信息。研究报告显示&am…

展台模型设计过程中会遇到那些问题?---模大狮模型网

在展台模型设计过程中&#xff0c;可能会遇到一些常见问题&#xff0c;包括但不限于&#xff1a; 一&#xff1a;空间规划问题 设计师需要确保展台布局合理&#xff0c;能够满足参展方的需求&#xff0c;同时还要考虑观众流线和空间利用效率。解决方法包括对空间进行良好的规划…

如何选择程序员职业赛道

目录 前言1 个人技能分析1.1 技术栈评估1.2 经验积累1.3 数据科学能力 2 兴趣与价值观2.1 用户交互与界面设计2.2 复杂问题解决与系统优化 3 长期目标规划4 市场需求分析4.1 人工智能和云计算4.2 前沿技术趋势 5 就业前景5.1 前端在创意性公司中的应用5.2 后端在大型企业中的广…

pytest 教程

1. 安装pytest 目前我使用的python版本是3.10.8 pip install pytest命令会安装下面的包&#xff1a; exceptiongroup-1.2.0-py3-none-any.whl iniconfig-2.0.0-py3-none-any.whl packaging-23.2-py3-none-any.whl pluggy-1.4.0-py3-none-any.whl pytest-8.0.2-py3-none-any.…