一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情
前言
这是kotlin查漏补缺系列的第二篇,关于kotlin泛型的分享,看完本文你将学会
- 1.kotlin的泛型使用
- 2.泛型的协变和逆变,kotlin的in和out
- 3.where,reified关键字的使用
认识泛型
什么是泛型
泛型的核心是类型参数化,目的跟函数的参数一样,提高复用性和通用性
例如集合类,假设没有泛型,我们就需要为String,View,Fragment等所有需要用到集合的类型都创建一个对应的集合类,这成本之高无法想象,所以就想到了能不能像函数一样,把集合的核心逻辑抽象为一个通用的集合类,等具体使用的时候再通过传参的方式指定是什么类型,所以就有了泛型类
泛型的声明可以在类,接口以及方法中,分别叫做泛型类,泛型接口,泛型方法
// 泛型类的声明
class TypeTest<T>() {
val mData:T? = null
fun getData(param:T):T{
return param
}
}
// 泛型方法的声明
fun <T> getSuccess(param:T):T{
return param
}
复制代码
有人可能有疑惑了,如果说通用型,直接用最高级别的父类不就好了,例如Java中的Object,Kotlin中的Any?,说的没毛病,你可以往最高级别的父类引用里面塞任何类型的值,但仔细想想,这样做是不是太难用而且也太危险,如果你只是往里面写,从不读,没问题,随便写,但是只要你想要往外读,你怎么读?你获取一个Object类型的对象,这个没意义吧,那你直接强转?搞不好就崩了。所以这就是泛型所要解决的问题
泛型的协变和逆变
先看个简单的栗子
open class A
open class B:A()
fun getType(param:TypeTest<A>){
}
// 根据多态性子类对象可以赋值给父类引用,所以这句没问题
val a1:A = B()
getType(TypeTest<A>())
getType(TypeTest<B>()) //IDE报错
复制代码
栗子中,B
是A
的子类,所以按照类的多态性,我们可以把B
的对象赋值给类型为A
的变量a1
但是上述代码最后一行getType(TypeTest<B>())
缺标红报错,显示类型不匹配,也就是这里TypeTest<B>
并不是TypeTest<A>
的子类,为什么呢?这是因为泛型本身的不可变性
但实际开发中,确实也会有这样的逻辑需求,怎么解决?这就是涉及到泛型的协变和逆变
java的泛型通配符
先来看看java的解决方式:泛型通配符,包括? extends
和?Super
? extends
称之为上界通配符,限制了泛型的父类型,这样做的目的是使泛型具有协变性(协变covariant这个词是翻译过来的,实在没搞懂怎么跟具体的含义对应起来),那到底有啥用?看下面的栗子
List<? extends TextView> views = new ArrayList<Button>();
TextView view = views.get(0);
views.add(new Button(context)); //IDE报错
复制代码
如上,Button
是TextView
的子类,views
是使用了上界通配符的List
变量,这时泛型类型为Button
的List
对象是可以正常赋值给views
的,这就是具有了协变性;
我们使用views
的读操作get
也能正常读取到一个类型为TextView的变量,因为views
中存储的一定是TextView
子类型的list,那我们获取的时候,获取到的结果一定能向上转型为TextView
类型,所以读操作是允许的,但是如果想往这个views
中使用写操作add一个新的对象,IDE就直接报错了,不让这样做,这是为什么?
因为我们可以将任何类型为TextView
的子类型的list赋值给views
的变量,但是对于编译器来说,只知道这个list里面存的是TextView
的子类型,并不知道具体是哪个类型,假设我们赋值的是一个Button
类型的list,我们再尝试往里面add一个EditText
(也是View的子类),那运行的时候肯定就得报错了,所以对于上界通配符来说add操作是不安全的,自然就不允许
? super
下界通配符,这里限制了泛型的子类型,目的是使java泛型具有逆变性
List<? super TextView> views1 = new ArrayList<View>();
views1.add(new Button(context));
Button btn = (Button) views1.get(0);
复制代码
下界通配符的特性刚好跟上界通配符相反,可以写操作,不让读操作,所以可以add添加元素,但是get只能获取Object
类型,看上面的例子,实际对象的泛型是View
,View
是TextView
的父类,自然也是TextView
的子类的父类,所以编译器认为往list中添加TextView
及其子类的对象都是能正确赋值的不会出问题
而读操作,除非你强转成object类型或者其他共同的父类不会出错,其他情况都是不安全的
kotlin的in 和 out
kotlin为了实现协变和逆变性,引入了两个关键字 in out
out
用来支持协变,等同于 Java 中的上界通配符 ? extends
;out很形象,用于往外取,也就是读操作
in
用来支持逆变,等同于 Java 中的下界通配符 ? super
,in也很形象,用于写入操作
open class A
open class B: A()
class C:B()
val type1:TypeTest<in B> = TypeTest<A>()
val type2:TypeTest<out B> = TypeTest<C>()
复制代码
声明处型变
在java中,我们注意到通配符只能用在声明泛型类对象的时候,但是有的时候这种特性会让写起来很麻烦,例如下面这种泛型类
class ReadClass<T> {
private T mData;
T getData(){
return mData;
}
}
复制代码
这个泛型类所对应的泛型只会用在读取操作上,所以下面这个操作实际上是安全的,但是IDE会报错
ReadClass<Object> objClass = new ReadClass<String>();
复制代码
因此在java中,这种情况都必须在每个声明的地方加上通配符才行,多少显得有点多余了
而kotlin针对这种情况做了优化,可以直接在类定义的时候就使用in和out,这样在具体使用的时候就不需要重复写了,这种特性叫做声明处型变
open class A
open class B: A()
class C:B()
// 在类的定义中声明之后,这个类的用途也就比较明确了,out用于读
class TypeOut<out T> {
val mData:T? = null
fun getData():T?{
return mData
}
}
// 泛型为子类的对象可以赋值给泛型为父类的引用,默认具备了协变性
val typeOut:TypeOut<B> = TypeOut<C>()
class TypeIn<in T> {
fun getData(param:T){
}
}
// 泛型为父类的对象可以赋值给泛型为子类的引用,即默认具备了逆变性
val typeIn:TypeIn<B> = TypeIn<A>()
复制代码
星投影
在使用泛型类的时候,如果不确定具体是什么类型,就可以用*
符号保证安全性,类似java泛型中的?
val type3:TypeTest<*> = TypeTest<A>()
复制代码
不过星投影结合上面的声明处型变相对java的?
就有些不一样的特征,不过这个是真的没怎么用过,我也就不深入介绍了
泛型约束
泛型约束主要是针对泛型的声明而言
在Java中是这样的
public <T extends CharSequence> void testFunc(T data){
}
复制代码
在kotlin中是这样的
fun <T : CharSequence> testFunc(data: T) {
}
复制代码
区别很简单,kotlin用:
取代了extends
泛型约束不要跟上面的协变弄混了,泛型约束用于声明的时候,java和kotlin都一样,表示我的这个泛型必须满足的父类型条件,两者是完全两个概念
where关键字
如果有多个限制条件,例如还必须是某些接口的实现类
java中的实现方式是使用&连接起来
public <T extends CharSequence & Serializable> void testFunc(T data){
}
复制代码
而在kotlin中,用到了where关键字
fun <T> testFunc(data: T) where T : CharSequence, T : Serializable {
}
复制代码
reified关键字
在java中,试试下面这个代码
private boolean isTypeT(Object param){
if(param instanceof T){ //IDE报错
}
}
复制代码
IDE会报错,也就是T并不能在代码中作为一个确定的类型来用。因为存在泛型擦出,也就是编译之后,字节码中并没有保留具体的泛型类型,自然就不让你去使用instanceOf这样无意义的判断了
在kotlin中类似的写法同样也会报错
fun isTypeT(param:Any):Boolean{
if(param is T){ //报错
}
}
复制代码
不过在kotlin中,可通过inline内联加上reified关键字实现这种逻辑
inline fun <reified E> isTypeE(param:Any):Boolean{
if(param is E){ //IDE不报错,可正常运行
}
}
复制代码
以上就是kotlin泛型的全部内容,泛型的内容还是很重要的,几乎无处不在,有不理解的地方建议多看几遍
感谢您的阅读,欢迎评论交流
参考大考文章