聊一聊Kotlin的泛型

article/2025/8/19 11:04:43

Kotlin的泛型

简介

与java一样,kotlin也支持泛型,用法和java泛型差别不大,kotlin特色是型变支持。

基本用法:

定义类:

跟java相同,定义在类后面的尖括号:

open class Basket<T>{}
定义方法:

定义在fun 关键字和 方法名之间。

//javapublic <S> void  testFunction(S s){//todo}
//kotlinfun <S> testFunction(s:S){//todo}

以声明一个水果篮为例,在构造方法中声明了泛型,里面提供一个list支持set和get操作:

open class Basket<T> {var content: T? = nullfun set(fruit: T) {content = fruit}fun get(): T? {return content}
}

定义一个水果类:

open class Fruit {open fun desc() {println("它是水果")}
}

使用:

fun main(args: Array<String>) {val fruit1 = Fruit()val basket = Basket(fruit1)
}
与java的尖括号语法不同,如果我在类的构造方法中指定了类型的话,在kotlin中可省略不写,其可帮我们自动推断。

从泛型类派生子类:

我现在写一个小水果篮子继承自果篮类:

class SmallBasket : Basket<Fruit>()
注意点:

与java不同的是,无论是通过显示指定还是让系统推断。kotlin要求始终为泛型参数明确地指定类型,所以上面参数我指定为水果类。
而在java中,以下两种都是允许的:

 public  class SmallBasketJ extends BasketJ<String> {}

 public  class SmallBasketJ extends BasketJ{}

型变:

回顾一下,java的泛型是不支持型变的,如何理解这句话呢?
首先这行代码是没有问题的:

String string = new String("sss");
Object object = string;

因为string是Object的子类,子类可以协变为父类,但是在泛型中:

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 不允许

因此,Java 禁止这样以保证运行时的安全,因为如果上面的代码允许被编译通过那么:

//这里我们把一个整数放入一个字符串列表
objs.add(1); 
//报 ClassCastException
String s = strs.get(0); 

所以泛型不支持型变的设计保证了其是“类型安全的”,但是通过通配符,可以让他们有“型变”的能力,具体为:

java通配符上限:
<? extends E>

表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以让Collection表示为Collection<? extends Object>的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)。

interface Collection<E> …… {void addAll(Collection<? extends E> items);
}

我们可以往Collection中添加E类型或者它的任意子类,这是泛型的协变。
我们用一个图来表示就是“正三角漏斗”,顶部就是我们的E:

image

意味着从泛型中取出(out)对象是安全的(一定是我们的E类型),但传入对象并不知道具体类型(可能是E或者它的子类)。

java通配符下限:
<? super T>

与通配符上限相反,限制传入的参数下限是T(即T或者它的父类)当我们用下限修饰符去修饰的话,将对象传给泛型对象是安全的,如Collections.copy方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {。。。。。。}

第一个参数使用通配符下限,限制了目标list只能是T或者T它的父类,而第二个参数拷贝源限制了参数只能是T或者T的子类,这样就保证了类型的合法。

        List<Apple> source = Arrays.asList(new Apple());//Object 也是最终父类 也ok
//        List<Object> destination = Arrays.asList(new Object());List<Fruit> destination = Arrays.asList(new Fruit());Collections.copy(destination,source);

我们上面的例子,我们可以完成这样的业务类型:

把包含苹果的List放入原有包含Fruit的List(或者Object的List).

通配符上限保证了传入参数的安全,如下“倒三角漏斗”所示:

image

我们可以往漏斗中放入E类型和任何它的父类,这就是泛型的逆变,意味着向其中传入(in)对象是安全的,但就不能保证取出来的参数的类型(可能是T,也可能是它的父类)

结论速记:

  • 通配符上限-extends-正三角-取出安全-out
  • 通配符下限-super-倒三角-存入安全-in
Kotlin型变:

无论java的通配符上限还是下限,都多少有缺陷,要么存不安全,要么取不安全,而在kotlin中,就解决了这个问题,让out:“纯输出” ,让in “纯输入” 。

在此之前,我们借助上面java的通配符的 (in) 和 (out) 的操作来理解一个概念:
我们称只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。

Kotlin声明处型变:
out: (协变注解)生产者:

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C 可以安全地作为 C的超类。
简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者。

还是水果篮和水果的例子,我定义了一个水果篮类,构造方法传入了T的示例,仅提供了get方法:

class Basket2<out T>(private val content: T) {fun get(): T {return content}
}

那么我们之前在java中不能实现的泛型型变,现在就是ok的了:

var basketFruit: Basket2<Fruit> = Basket2(Fruit())
var basketApple: Basket2<Apple> = Basket2(Apple())//ok的 符合协变的规则
basketFruit = basketApple
in: (逆变注解)消费者:。它使得一个类型参数逆变:只可以被消费而不可以被生产,我们以Compareble为例:
interface Comparable<in T> {operator fun compareTo(other: T): Int
}fun demo(x: Comparable<Number>) {// 我们可以将 x 赋给类型为 Comparable <Double> 的变量val y: Comparable<Double> = x // OK!因为 y可以接受Double或者它的任意父类,即“逆变了”
}

我们再回到篮子和水果的例子,我定义一个水果篮类,用in修饰:

class Basket3<in T> {fun set(param: T) {println(param)}
}

那么我们现在可以逆变了:

var basket3Apple = Basket3<Apple>()
var basket3Fruit = Basket3<Fruit>()
//ok的 符合逆变
basket3Apple = basket3Fruit
结论:
  • 如果泛型T(或其他字母)只出现在该类的返回值中声明,那么该泛型形参即可使用out修饰
  • 如果泛型T(或其他字母)只出现在该类的方法的形参声明中,那么泛型形参可使用int修饰
Kotlin使用处型变:类型投影

声明时型变虽然方便,但它有一个限制:要么该类的所有方法都只用泛型声明返回值类型(此时可用out声明型变):要么所有方法都只用泛型声明形参类型(此时可用in声明型变)。如果一个类中有 的方法使用泛型声明返回值类型,有的方法使泛型声明形参类型,那么该类就不能使用声明处型变。典型的例子就是Kotlin 的Array类,它无法使用声明处型变,该类在T 上既不能协变也是不能逆变的。

class Array<T>(val size: Int) {fun get(index: Int): T { …… }fun set(index: Int, value: T) { …… }
}

假如写下了如下方法,把一个数组复制到另外一个数组:

fun copy(from: Array<Any>, to: Array<Any>) {assert(from.size == to.size)for (i in from.indices)to[i] = from[i]
}

尝试着按照这种方式调用:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 其类型为 Array<Int> 但此处期望 Array<Any>

再次回到老问题:T是不型变的,因此 Array和Array都不是彼此的子类,如果在from参数中要求Sting,我实际却传入了int就会报ClassCastException,那么我们想避免这样的事情发生我们可以这么做:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

我们说from不仅仅是一个数组,而且是一个受限制(投影)的数组,我们只可以调用返回类型为T的方法,上面我们就只能调用get()。这便是我们的使用处型变的方法。

再例如:
我定义一个类型为Number的Array:

var numArr: Array<Number> = arrayOf(1,2,3,4,5)
numArr.set(0,2)//1.正常
var intArr:Array<Int> = arrayOf(1,2,3)//2.正常
numArr = intArr//3.报错 不支持声明处型变

那么我现在在Number上面加上一个out

var numArr: Array<out Number> = arrayOf(1,2,3,4,5)
numArr.set(0,2)//1.报错
var intArr:Array<Int> = arrayOf(1,2,3)//2.正常
numArr = intArr//3.正常

我用out修饰了Number,意味着它可以接受协变,代价就是只能出不能添加。

上面的例子中 out 的定义就叫类型投影

依然以Array为例:

我写一个填充到数组的方法,指定类型为String

fun fill(dest: Array<String>,value:String){if(dest.size>0){dest[0] = value}
}

此时我这么调用就会报错:

var arr1:Array<CharSequence> = arrayOf("a","b",StringBuilder("test"))
fill(arr1,"test") //报错
println(arr1.contentToString())

此时我在声明处添加 in ,表示可以接受String的任何父类,就可以编译通过了:

fun fill(dest: Array<in String>,value:String){
。。。。。。
}

再例如,刚刚上面的的例子:

var intArr:Array<Int> = arrayOf(1,2,3)
var number:Array<Number> = arrayOf(1,2,3)
intArr = number //报错:不支持声明时逆变

我们加上这个限制以后,就能逆变了:

var intArr:Array<in Int> = arrayOf(1,2,3)
星投影

表示不知道类型实参的任何信息

var list:Array<*> =  arrayOf("test","kotiln",1,2)
list[0]="1"//报错 无法被写入

所以:

  • 星号投影不能写入,只能读取
  • <*>等价于java中的<?>
设定类型形参上限
单个形参

kotlin不仅允许在使用通配符时设定形参上限,而且可以在定义类型形参时设定上限,用于表示给该类型的实际类型要么是该上限类型,要么是它的子类。

回顾一下上面篮子的例子

class Basket2<out T>(private val content: T) {fun get(): T {return content}
}

我们改一改,让它只能放水果:

class Basket2<T:Fruit>(private val content: T) {fun get(): T {return content}
}

乍一看,它们好像没有什么区别。。。。

var basket2Fruit: Basket2<Fruit> = Basket2(Fruit())
var basket2Apple: Basket2<Apple> = Basket2(Apple())

以上两行代码在两种修饰符下都可以执行,但是,我们知道,out是可以让泛型协变的,即:

basket2Fruit = basket2Apple //out ok ,设定形参上限报错

用out是ok的,代价是只能作为生产者输出了,而用形参上限,我们却可以跟它提供一个set方法:

class Basket2<T : Fruit>(private var content: T) {fun set(fruit: T) {content = fruit}fun get(): T {return content}
}

这样我们就能保证这个篮子中只能放入Fruit和它的子类了,也能从里面取出Fruit,但是此时,它不能型变。

多个形参

kotlin允许为类型设定多个形参上限,在尖括号外用 where语句:
先定义两个接口或者父类:

interface Eatable {fun eat()
}interface Color

如果想限定参数的必须实现上面两个接口可以这么写:

class Basket<T> where T : Eatable, T : Color {
。。。。。。
}
  • 对于泛型的使用如果我们没有型变需要,有存有取,可以优先使用形参上限来限制参数。
具体化类型参数

kotlin允许在内联函数(inline修饰)使用refied修饰泛型参数,这样可将泛型参数变成一个具体的类型参数,很适用于我们需要用Class做参数的情形:
例如,我们要从某个List找某个指定类型的元素:

fun<T> findData(clazz:Class<T>):T?{.....
}
//使用
findData(Integer:class.java)

那么这么写就能省略class参数了:

fun inline <refied T> findData():T{.....
}
//使用
findData<Int>()

是不是优雅许多?

参考资料:

https://www.kotlincn.net/docs/reference/generics.html


http://chatgpt.dhexx.cn/article/MG0wSWDJ.shtml

相关文章

聊一聊学习方法

聊一聊学习方法 为什么学习没效果&#xff1f;你只是看起来很努力自知与不自知&#xff0c;真的不是一字之差什么时候真的能出效果&#xff1f;学习方法一&#xff08;拍照式记忆&#xff09;学习方法二&#xff08;殊途同归&#xff09;学习方法三&#xff08;精细化管理&…

聊一聊ThreadLocal内存泄漏的问题

回答任何一个问题的时候应该要遵循&#xff1a;明确题意-->深入浅出-->举例说明-->总结&#xff0c;这四个步骤很重要&#xff0c;可以让你沉着冷静&#xff0c;思路清晰&#xff0c;避免尴尬。 01 — 明确题意 明确题意的意思就是先明确一下面试官的题目&#xff0…

聊一聊Https

前言 https协议是一种在http的基础上进行加密的协议&#xff0c;在http协议传输过程中&#xff0c;传输的数据都是已明文的方式进行传输&#xff0c;那我们的信息就有可能被他人进行捕获篡改&#xff08;比如你给女神表白却被程序员情敌发现并修改了你的消息&#xff0c;然后你…

SLAM基础——聊一聊信息矩阵

文章目录 前言1 :book: 信息矩阵1-1 :bookmark: 信息矩阵是什么&#xff1f;有什么作用&#xff1f;1-2 :bookmark: 信息矩阵与Hessian矩阵的关系1-2-1 Hessian矩阵和H矩阵的关系1-2-2 Hessian或H矩阵和信息矩阵的关系 2 :book: 信息矩阵与最小二乘的关联2-1 :bookmark: 先谈一…

聊一聊SpringCloud的五大组件

聊一聊SpringCloud的五大组件 1.初始SpringCloud 微服务是一种架构方式&#xff0c;最终肯定需要技术架构去实施。 微服务的实现方式很多&#xff0c;但是最火的莫过于Spring Cloud了。为什么&#xff1f; 后台硬&#xff1a;作为Spring家族的一员&#xff0c;有整个Spring全…

聊一聊Serverless

聊一聊Serverless Serverless是什么云计算发展过程Serverless的优势Serverless的不足云函数冷启动热启动函数实例不同语言冷启动时长排名首次调用超时性能优化 SFFBaaS再会推荐阅读 Serverless是什么 从单词角度理解,server译为服务&#xff0c;less译为少&#xff0c;Serverl…

聊一聊C语言位域/位段

目录 1、概念和定义 2、实例 在做嵌入式开发的时候&#xff0c;我们经常会遇到这样的代码&#xff1a; struct {unsigned int widthValidated : 1;unsigned int heightValidated : 1; } status; 这样定义结构体变量是什么意思呢&#xff1f; 主要原因是&#xff1a;有些信…

【毕业季】今天简单聊一聊这几个问题

今天简单聊一聊这几个问题 我又来参加活动啦~ 活动地址&#xff1a;毕业季进击的技术er 首先我看了下活动模板&#xff0c;有三个身份&#xff0c;| 毕业生 | 在校生 |职场人 &#xff0c;现在呢其实我们应该还算是在校生&#xff0c;但是我们是大三&#xff0c;也可以说是大四…

聊一聊差分放大器

目录 1、共模抑制比&#xff08;CMRR&#xff09; 2、低容差电阻 3、高噪声增益 4、单电容滚降 5、运算放大器输入端之间的电容 大学里的电子学课程说明了理想运算放大器的应用&#xff0c;包括反相和同相放大器&#xff0c;然后将它们进行组合&#xff0c;构建差动放大器…

简单聊一聊单点登录

单点登录 一、单点登录二、演示步骤①简单结构(域名的设置)②创建项目1.sso-server 模块代码LoginController代码如下GulimallTestSsoServerApplication启动类代码login.html代码application.properties配置文件pom.xml文件 2.sso-client1模块代码HelloController代码GulimallT…

聊一聊俞敏洪

2009年&#xff0c;CCTV颁发中国经济年度人物&#xff0c;给出了这样的颁奖词&#xff1a; “一个曾经的留级生&#xff0c;让无数学子的人生升级&#xff1b;他从未留过洋&#xff0c;却组建了一支跨国的船队。他用26个字母拉近了此岸和彼岸的距离。胸怀世界&#xff0c;志在东…

闲暇聊一聊

大家好&#xff0c;我是Tom哥 非常庆幸&#xff0c;早毕业了几年&#xff0c;那时的互联网还没有像现在这么卷&#xff0c;甚至没有 内卷 这个网络流行词 一切都是那么美好&#xff0c;可以自由享受学习技术的快乐 Tom哥是校招进的阿里&#xff0c;当时的技术资料可不像现在…

聊一聊数据库的行存与列存

目录 存储方式比较 优缺点比较 行存与列存实验 选择建议 注意事项 好多人最开始学习数据库的时候&#xff0c;是关系数据库&#xff0c;数据以表格形式存储&#xff0c;一行表示一条记录。其实这种就是典型的行存储&#xff08;Row-based store&#xff09;&#xff0c;将…

聊一聊 AS 的一些好用的功能

聊一聊 AS 的一些好用的功能 文章开始前先墨迹几句&#xff0c;好久没写文章了&#xff0c;这段时间公司确实挺忙&#xff0c;也没抽出时间&#xff0c;上一篇文章还是三月初写的&#xff0c;距今已经两个多月啦&#xff0c;不能再这样下去了&#xff0c;虽然我不能像一些大佬…

聊一聊罗振宇

小灰是得到平台的重度用户&#xff0c;也是罗振宇的粉丝。 罗振宇&#xff0c;人称“罗胖子”&#xff0c;非常有趣的是&#xff0c;人们常常把另一个姓罗的胖子和他混为一谈。 今天&#xff0c;我们就来聊一聊他。 罗振宇是个特别复杂的人。 很多人认识罗振宇&#xff0c;是从…

聊一聊我的2020

回顾2020 &#xff08;接下来是一大段唠叨&#xff0c;可以直接到文末&#xff0c;有小安利和经验总结&#xff09; 还有几天2020年就结束并迎来新的一年。这一年中总是能听到有人在感慨2020是不平凡的一年。仔细想想&#xff0c;确实如此&#xff0c;至少对我来说是这样的。 作…

聊一聊 gRPC 中的拦截器

今天我们继续 gRPC 系列。 前面松哥跟大家聊了 gRPC 的简单案例&#xff0c;也说了四种不同的通信模式&#xff0c;感兴趣的小伙伴可以戳这里&#xff1a; 一个简单的案例入门 gRPC 聊一聊 gRPC 的四种通信模式 今天我们来继续聊一聊 gRPC 中的拦截器。 有请求的发送、处理&…

聊一聊微博新知博主这件事,看看赚钱方式有哪些?

从今天开始&#xff0c;准备将我付费星球内的精华文章&#xff0c;在每周六和周日以付费文章的方式在公众号分享给大家&#xff0c;如果你不想加入我的星球&#xff0c;还想看的话&#xff0c;可以在这里付费看。当然&#xff0c;加入星球会更划算&#xff0c;因为星球内内容更…

【QGIS入门实战精品教程】2.1:初识QGIS软件

从今天开始&#xff0c;我们一起来学习一款免费开源、对机器要求低、功能强大的GIS软件&#xff1a;QGIS &#xff01; 一、QGIS简介 QGIS&#xff08;原称Quantum GIS&#xff09;是一个自由软件的桌面GIS软件。它提供数据的显示、编辑和分析功能。 QGIS是一个用户界面友好的…

QGIS教程01:为什么要用QGIS?

从去年开始抛弃ArcGIS转用QGIS以来&#xff0c;发现QGIS越来越好用&#xff0c;功能也相当强大。而且我发现身边好多朋友也在开始使用QGIS&#xff0c;但目前国内这方面的学习资料还比较少&#xff0c;国外的原版资料又相对比较啰嗦&#xff0c;所以我和几位GISer入门知识星球的…