新手上路,Kotlin学习笔记(八)---泛型的使用

        入行没几年的小码农,近期学习Kotlin,做一份笔记记录,此文依据《Kotlin实战》这本书的流程记录,部分示例内容均摘自《Kotlin实战》,记下自己的理解,本篇记录在Kotlin中泛型的相关使用方式。


 

        Kotlin学习笔记系列

        新手上路,Kotlin学习笔记(一)-- Kotlin入门介绍

        新手上路,Kotlin学习笔记(二)---方法(函数)部分

        新手上路,Kotlin学习笔记(三)---类、对象、接口

        新手上路,Kotlin学习笔记(四)---Lambda表达式在Kotlin中的使用

        新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统

        新手上路,Kotlin学习笔记(六)---运算符重载和其它约定

        新手上路,Kotlin学习笔记(七)---Lambda作为形参和返回值的使用

        新手上路,Kotlin学习笔记(八)---泛型的使用

        新手上路,Kotlin学习笔记(九)---注解和反射

一、Kotlin中泛型的介绍

        在Java中,对于泛型我们应该很熟悉了,使用最频繁的就是在集合中使用泛型。

        我们先介绍一下:泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。

        通过上面介绍,我们可以理解为,我们看到的List<T>这里的T就是类型形参,当创建一个实际的List时,比如List<String>,这里的String就是类型实参。所以,类型形参只是声明使用的,实际使用的时候还是类型实参!

        在声明泛型的时候,和Java的使用方式基本一致,使用尖括号即可,当一个类继承泛型类或实现一个泛型接口,需要显式的为其指定一个类型实参(可以是一个具体的类型或者另一个类型形参)。

二、类型参数约束

        类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。

        如果把一个类型指定为泛型类型形参的上界约束,那么在泛型类型具体的初始化中,其对应的类型实参必须是这个具体类型或者子类型。

        可以理解为,我们为这个泛型指定了一个上限(父类),其最大的范围只能是这个类,在Java中我们会用extends来表示一个泛型的范围,如List<T extends People>,在kotlin中的对应表示就是List<T : People>,此时这个List中的实例只能是People的子类或者People类型本身。并且可以直接调用这个上界的方法,如:

    fun <T : Comparable<T>> max (first : T , second : T) : T
    {
        return if (first > second) first else second
    }

        当我们声明其上界为Comparable的时候,我们可以直接调用Comparable才有的重载算术运算符方法 >

        在Kotlin中,我们还可以为一个泛型定义多个约束,这样传入的类型实参将需要同时实现多个接口,如下面示例:

    fun <T> ensureTrailingPeriod(seq : T) where T : CharSequence, T : Appendable
    {
        if(!seq.endsWith("."))
        {
            seq.append(".")
        }
    }

        用where关键字来限定泛型的多个上界,这样说明作为类型实参传入的参数一定实现了CharSequence和Appendable接口,我们就可以同时调用这两种接口的方法了。

        tips:对于没有显示声明泛型上界的情况,其会拥有默认的上界Any?,所以我们在使用的时候是需要做非空判断的,如果不希望其为空,应显式的声明为Any

三、运行时的泛型

        1、泛型的类型检查和转换

        我们知道,在JVM上,对于泛型,是通过类型擦除实现的,就是泛型类实例的类型实参在运行时时不可保留的。比如一个List<String>,在运行时只能看到是一个List,并不能识别出包含的是哪种元素。所以我们不能在is检查中使用类型实参中的类型,下面代码将不能被编译:

        当然,JVM擦除泛型类型信息是有好处的,要保存在内存中的类型信息会更少,所以应用程序的内存总量会少一些。

        因为Kotlin不允许使用没有指定类型实参的泛型类型,此时又不能使用类型实参,那么当我们需要只需要判断一个集合是List或者Set该怎么办呢?

        此时可以使用星号投影,如下所示:

    fun <T : Person> checkPerson(p : Collection<T>)
    {
        if (p is List<*>)
        {
            //这样写是被允许的,此处判断了p是一个List
        }
    }

        上面是is使用的场景,当使用关键字as的时候,编译器将允许我们书写一般的泛型类型的转换,但是如果该类有正确的基础类型,但是类型实参是错误的,转换不会报错,还是因为在运行时类型实参是未知的,当我们再次使用的时候,将会抛出异常!

    fun printSum(c : Collection<*>)
    {
        val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
        print(intList.sum())
    }

        此处编译器会提示警告,但是允许我们这样书写。

        上述代码,在执行的时候,如果传入的不是List,将会是我们设想的弹出IllegalArgumentException,如果传入的是一个List<String>,intList的类型转换将是成功的,而在后续进行sum操作的时候,报错ClassCastException。

        tips;对于已知的类型实参的集合,编译器是足够智能的,所以我们可以使用is进行转换,如:

    fun checkIs(c : Collection<Int>)
    {
        if(c is List<Int>)
        {
            //这里的判断是允许的,因为传进来的一定是Int类型实参的集合,运行时不知道类型实参也不影响该判断
        }
    }

        2、声明带实化类型参数的函数

        当一个函数是泛型函数的时候,我们刚刚知道了,其在运行的时候会被擦出类型实参,所以我们就无法获知这个类型实参是什么,下面的代码将无法编译:

        针对这种情况,Kotlin为我们提供了一种解决方式,那就是使用内联函数,上一篇博客我们已经知道了,使用inline标记的内联函数,将会在调用的时候替换为函数实际代码的实现,所以我们将上述函数声明为inline并用reified标记类型参数,就可以检查value是不是T的实例。

    inline fun <reified T> isA(value : Any) : Boolean
    {
        return value is T
    }

四、变型:泛型和子类型化

        前面我们在介绍泛型的参数约束时提到,在泛型类型的具体初始化时,必须是这个类型或者其子类型。我们说的是子类型,而不是子类!子类型和子类是有区别的!

        考虑下面一个场景,此时有这样一个方法:

    fun addAnswer(list : MutableList<Any>)
    {
        list.add(42)
    }

        如果我们传入一个类型为List<String>的对象进去,代码不就会报错了吗?所以子类型并不是单纯的子类。

        子类型:任何时候如果需要的是类型A的值,你都能够使用类型B的值(当作A的值),类型B就称为类型A的子类型。

        根据定义,我们就知道了Int是Number的子类型,是Int?的子类型,同样的,Int也是Int本身的子类型。

        1、协变

        一个协变类是一个泛型类(我们以Product<T>为例),如果A是B的子类型,那么Product<A>就是Product<B>的子类型。

        Kotlin中要声明一个类在某个参数上是可协变的,在该类型参数前面加上out关键字即可。如List这个类

    public interface List<out E> : Collection<E> 

        之前我们学过,List在kotlin中是只读的,只会在返回值处使用泛型,所以在不会出现MutableList这种被改变数据后出现异常的场景。此时List是协变的没有任何问题。

        2、逆变

        我们来看Comparable接口,该接口只在方法内部使用了泛型,并没有像List那样在返回值处使用。

/**
 * Classes which inherit from this interface have a defined total ordering between their instances.
 */
public interface Comparable<in T> {
    /**
     * Compares this object with the specified object for order. Returns zero if this object is equal
     * to the specified [other] object, a negative number if it's less than [other], or a positive number
     * if it's greater than [other].
     */
    public operator fun compareTo(other: T): Int
}

        此时,该接口的泛型声明处加了关键字in,并且我们也知道,一个要求比较Any的对象,不能传入Comparable<String>。但是一个String的比较,既可以传入Comparable<String>,又可以传入Comparable<Any>,这与之前的协变刚好相反,这种情况称作逆变。

        3、不变性

        相对于协变和逆变,MutableList这种类,即将泛型当做返回值,又作为参数进行修改使用,我们称之为不变型

        

        同时,也存在协变和逆变并存的场景,在一个类中,一个协变,一个逆变,如Function1这个类:

/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

        P1这个类型用在了方法的参数中,R则使用在返回值处。

        4、点变型

        在一个函数中,如果有多个参数都使用同一个泛型,但是某个参数实际只使用了协变或者逆变的场景,同样可以使用in或者out进行修饰,这种方式成为点变型,如:

    fun <T> copyData(source : MutableList<out T>, destination : MutableList<in T>)
    {
        for(item in source)
        {
            destination.add(item)
        }
    }

        参数source只调用了读取的函数,destination只调用了写入的函数,所以可以对它们单独的声明协变和逆变,并且在使用的时候只允许调用相对应的函数,否则编辑器将报错。

        5、星号投影

        前面我们介绍了使用星号*的场景,其实,MutableList<*>星号投影就相当于MutableList<out Any?>的场景,此时可以读取但是不行有任何修改的实现。

        tips:最后,我们所进行依据的逆变判定是针外提供的方法的,对于私有方法,其一个对象内部一定是同一个类型,所以如果对泛型有修改的操作都是私有方法,那么这个泛型类还是可以声明为协变的!

        本章的内容就到这里了,下一章我们将学习kotlin中的注解和反射。

        //下一章  新手上路,Kotlin学习笔记(九)---注解和反射

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值