kotlin查漏补缺系列(2)——泛型

一起养成写作习惯!这是我参与「掘金日新计划 · 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报错
复制代码

栗子中,BA的子类,所以按照类的多态性,我们可以把B的对象赋值给类型为A的变量a1

但是上述代码最后一行getType(TypeTest<B>())缺标红报错,显示类型不匹配,也就是这里TypeTest<B>并不是TypeTest<A>的子类,为什么呢?这是因为泛型本身的不可变性

但实际开发中,确实也会有这样的逻辑需求,怎么解决?这就是涉及到泛型的协变和逆变

java的泛型通配符

先来看看java的解决方式:泛型通配符,包括? extends?Super

扫描二维码关注公众号,回复: 13798263 查看本文章

? extends

称之为上界通配符,限制了泛型的父类型,这样做的目的是使泛型具有协变性(协变covariant这个词是翻译过来的,实在没搞懂怎么跟具体的含义对应起来),那到底有啥用?看下面的栗子

    List<? extends TextView> views = new ArrayList<Button>();
    TextView view = views.get(0);
    
    views.add(new Button(context)); //IDE报错
复制代码

如上,ButtonTextView的子类,views是使用了上界通配符的List变量,这时泛型类型为ButtonList对象是可以正常赋值给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类型,看上面的例子,实际对象的泛型是ViewViewTextView的父类,自然也是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泛型的全部内容,泛型的内容还是很重要的,几乎无处不在,有不理解的地方建议多看几遍

感谢您的阅读,欢迎评论交流

参考大考文章

Kotlin 的泛型

Kotlin泛型,有你想了解的一切

重学Kotlin之泛型的逆变和协变

猜你喜欢

转载自juejin.im/post/7088219237457592350