泛型

作者: 凌寒天下独自舞 | 来源:发表于2018-11-05 18:11 被阅读0次

    与Java泛型相同,Kotlin同样提供了泛型支持。对于简单的泛型类、泛型函数的定义,Kotlin 与 Java 的差别不大 。Kotlin 泛型的特色功能是型变支持, Kotlin 提供了声明处型变和使用处型变两种支持 ,而Java 只支持使用处型变。

    泛型入门

    定义泛型接口、类

    可以为任何类、接口增加泛型声明。下面自定义一个 Apple 类,这个Apple类就可以包含一个泛型声明。

    //定义 Apple 类时使用了泛型声明
    open class Apple<T> {
        //使用泛型 T 定义属性
        open var info: T?
    
        constructor() {
            info = null
        }
    
        //下面方法中使用泛型 T 来定义构造器
        constructor(info: T) {
            this.info = info
    
        }
    }
    
    fun main(args: Array<String>) {
        //由于传给泛型 T 的是 String ,所以构造器的 参数只能是 String
        var a1 :Apple<String> = Apple("苹果")
        println(a1.info)
    
        //由于传给泛型 T 的是 Int ,所以构造器的 参数只能是 Int
        var a2:Apple<Int> = Apple(10)
        println(a2.info)
    
        //由于构造器的参数是 Double,因此系统可推断出泛型形参为 Double 类型
        var a3 = Apple(3.5)
        println(a3.info)
    }
    

    从泛型类派生子类

    当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类派生子类。需要指出的是,当使用这些接口、父类时不能再包含泛型形参。例如,下面代码就是错误的(Apple类必须有 open修饰才能派生子类)。

    //定义类 A 继承 Apple 类, Apple 类不能还使用泛型形参
    class A: Apple<T>()
    

    在定义类、接口、方法时可以声明泛型形参,在使用类、接口、 方法时应该为泛型形参传入实际的类型。
    如果想从 Apple类派生一个子类,则可以改为如下代码 :

    class A: Apple<String> ()
    

    与Java不同的是,Kotlin要求始终为泛型参数明确地指定类型,而不管是通过显式指定,还是让系统进行推断。例如,如下代码是错误的。

    //系统无法推断出 T 是何种类型,因此编译报错
    var a4 = Apple ()
    
    //使用 Apple 类时,没有为泛型 T 传入实际的类型参数,编译报错
    public class A extends Apple
    

    如果从 Apple<String>类派生子类,则在 Apple 类中所有使用泛型 T 的地方都将被替换成 String类型,即它的子类将会继承 String类型的info属性。如果子类需要重写父类的属性或方法,就必须注意这一点。

    class A1: Apple<String>() {
        override var info: String? =null
            get() ="子类"+ super.info
    
    }
    

    型变

    Java 的泛型是不支持型变的,Java采用通配符来解决这个问题;而 Kotlin 则采用安全的型变代替了 Java 的通配符。

    泛型型变的需要

    首先回顾一下 Java 泛型的特征 : Java 的泛型是不支持型变的。通俗地说,List<String>并不是 List<Object>的子类,因此 List<String>不能直接赋值给 List<Object>。
    但 Java取消型变之后,程序变得非常麻烦。比如 Java的 Collection中有一个 addAll()方法, 该方法负责将另一个集合中的所有元素添加到本集合内。假如 Java将该方法定义为如下形式:

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

    那么如下代码也是不能运行的。

    Set<Number> numSet = new HashSet<> () ; 
    Set<Integer> intSet =new HashSet<>();
    numSet.addAll(intSet);
    

    为了处理型变的需要,Java采用通配符方式进行处理,它将 addAll()方法定义为如下形式:

    addAll(Collection<? extends E> c)
    

    此时 addAll()方法的参数类型是指定上限的类型,其本质就是为了支持型变,因此上面代码可以正常运行。

    泛型存在如下规律:

    • 通配符上限(泛型协变)意味着从中取出( out)对象是安全的,但传入对象( in)则不可靠。
    • 通配符下限(泛型逆变)意味着向其中传入(in)对象是安全的,但取出对象(out)则不可靠。

    Kotlin利用上面两个规律,抛弃了泛型通配符语法,而是利用 in、 out来让泛型支持型变。

    声明处型变

    Kotlin 处理泛型型变的规则很简单:

    • 如果泛型只需要出现在方法的返回值声明中(不出现在形参声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限):如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用 out修饰。
    • 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限):如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用 in修饰。

    下面程序先定义一个支持泛型协变的类。

    class User<out T> {
        //此处不能用 var,否则就有 setter 方法
        // setter 方法会导致 T 出现在方法形参中
        val info: T
    
        constructor(info: T) {
            this.info = info
        }
    
        fun test(): T {
            println("执行 test方法")
            return info
        }
    }
    
    
    fun main(args: Array<String>) {
        //此时 T 的类型是 String
        var user = User("kotlin")
        println(user.info)
        //对于 u2 而言,它的类型是 User<Any>,此时T的类型是Any
        //由于程序声明了T支持协变,因此User<String>可当成 User<Any>使用
        var u2 =user
    }
    

    上面程序中代码声明了一个泛型类,且使用了out 修饰泛型形参,因此在该User类的内部,T只能出现在方法的返回值声明中,不能出现在方法的形参声明中。所以如果用T为User类声明属性,则只能声明为只读属性,否则setter方法 的形参类型是T,这就不符合要求了。

    一 旦声明了泛型类支持协变,程序即可安全地将 User<String>、 User<Int>赋值给 User<Any>,只要尖括号中的类型是Any 的子类即可。

    下面程序再定义 一个支持泛型逆变的类。

    class Item<in T> {
        fun foo(t: T) {
            println(t)
        }
    }
    
    fun main(args: Array<String>) {
        //此时 T 的类型是 Any
        var item = Item<Any>()
        item.foo(200)
    
        var im2 : Item<String> = item
        // im2 的实际类型是 Item<Any>,因此它的foo参数只要是Any即可
        // 但我们声明了 im2 的类型为 Item<String>
        // 因此传入的参数只可能是 String,所以程序肯定是安全的
        im2.foo("Kotlin")
    }
    

    上面代码声明了一个泛型类, 且使用了in修饰泛型形参,因此在该Item类的内部,T只能出现在方法的形参声明中,不能出现在方法的返回值声明中 。
    一旦声明了泛型类支持逆变 , 程序即可安全地将 ltem<Any>、 ltem<CharSequence>赋值给 User<Sting>, 只要尖括号中的类型是String 的父类即可。

    通过上面介绍不难发现 , Kotlin的处理规则很简单:

    • 如果泛型 T(或其他字母)只出现在该类的方法的返回值声明中 (T代表的是传出值),那么该泛型形参即可使用 out修饰 T。
    • 如果泛型 T(或其他字母)只出现在该类的方法的形参声明中(T代表的是传入参数),那么该泛型形参即可使用 in修饰 T。

    使用out修饰泛型的类支持协变,也就是可以将User<String>、User<Int>当成 User<Any>处理,只要尖括号中的类型是 Any 的子类即可; 使用 in修饰泛型的类支持逆变,也就是可以将Item<Any>、 ltem<CharSequence>当成 Item<String>处理,只要尖括号中的类型是String的父类就行。
    上面定义的 User、 Item 类都是在声明时使用 out 或 in 指定泛型支持型变的,因此这种方式被称为“声明处型变”。

    使用处型变:类型投影

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

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

    上面 Array 类的泛型参数 T 既要出现在 get()方法的返回值声明中,也要出现在 set()方法的形参声明中,因此该 Array类的泛型T既不能用out修饰,也不能用in修饰。
    而 List 集合则不同,由于 List 集合是一个只读集合,程序只需要从 List 集合中取出元素(不能添加元素),因此 T 只会出现在 List集合方法的返回值声明中。所以List集合可定义为支持协变。 List 的源代码片段如下 :

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

    如果不能使用声明处型变,则还可使用 Kotlin 提供的“使用处型变”。所谓使用处型变,就是在使用泛型时对其使用out或in修饰。
    由于 Array类本身不支持声明处型变,因此这里将会以 Array为例来讲解使用处型变。下面先看使用处协变(使用 out修饰)。

    fun copy(from: Array<out Any>, to: Array<Any>) {
        val length = if (from.size < to.size) from.size else to.size
        for (i in 0 until length) {
            to[i] = from[i]
        }
    }
    
    fun main(args: Array<String>) {
        var arr1 = arrayOf(1,5,7,0)
        var arr2:Array<Any> = arrayOf(4,78,23,9,10)
        copy(arr1,arr2)
        println(arr2.contentToString())
    }
    

    留意上面程序中 copy()函数的 from参数的声明,该 from 参数的类型是 Array<out Any>, 这就是使用处协变。也就是说,程序传入该from 参数的可以是 Array<Int>、 Array<String>等各种类型,只要尖括号中的类型是 Any 的子类即可,因此程序中from参数的是 Array<Int>。
    需要说明的是,如果将from参数声明为Array<out Any>类型,那么就意味着只能安全地从该from参数代表的数组中取元素,而不能将元素添加到from 数组中,道理很明显 : 我们无法预测实际传给from参数的是 Array<Int>还是 Array<String>。

    下面我们以 Array 为例来讲解使用处逆变(使用in修饰)。

    fun fill(dest:Array<in String>,value:String){
        if(dest.size>0){
            dest[0] = value
        }
    }
    
    fun main(args: Array<String>) {
        var arr:Array<CharSequence> = arrayOf("4","kotlin","java")
        fill(arr,"xxx")
        println(arr.contentToString())
    
        var intArr:Array< Int> = arrayOf(1,3,5,7,9)
        println(intArr.contentToString())
        intArr.set(0,34)
    
        var numArr:Array<Number> = arrayOf(3,4,5,1.4,2.8)
        //Array 不支持声明处型变,编译错误
        intArr =numArr
        println(intArr.contentToString())
    }
    

    星号投影

    星号投影是为了处理 Java 的原始类型,比如如下Java代码:

    ArrayList list = new ArrayList() ;
    

    虽然Java的List、 ArrayList 都有泛型声明,但程序并没有为它们传入类型参数,这在 Java 程序中是允许的。这种用法被称为“原始类型”。
    但在 Kotlin 中要写成如下形式。

    fun main(args: Array<String>) {
        //〈*〉必不可少,相当于 Java 的原始类型
        var list: ArrayList<*> = arrayListOf(1, "str")
        println(list)
    }
    

    关于星号投影,下面给出一些示例说明。

    • 假如定义了支持声明时型变的 Foo<out T>类,该泛型支持声明时协变,因此其中T是一个具有上限的协变类型参数, Foo<>等价于 Foo<outAny?>。这意味着当 T未知时,我们可以安全地从 Foo<>读取 Any?类型的值。
    • 假如定义了支持声明时型变的 Foo<in T> 类,该泛型支持声明时逆变,因此其中T是一个逆变类型参数, Foo<>等价于 Foo<in Nothing>。这意味着当T未知时,我们不能以任何安全的方式向 Foo <>写入值。
    • 假如定义了不支持声明时型变的 Foo<T>类,该泛型不支持型变。这意味着当T未知时, Foo<>在读取值时等价于 Foo<out Any?>,在写入值时等价于 Foo<in Nothing> (即不能以任何安全的方式向 Foo <>写入值)。

    泛型函数

    前面介绍了在定义类、接口时可以使用泛型形参,在该类、接口的方法定义和属性定义中, 这些泛型形参可被当成普通类型来用。在另外一些情况下,在定义类、接口时没有使用泛型形参,但在定义方法时想自己定义泛型形参,这也是可以的, Kotlin 提供了对泛型函数的支持。

    泛型函数的使用

    所谓泛型函数,就是在声明函数时允许定义一个或多个泛型形参,泛型形参要用尖括号括起来,整体放在 fun 与函数名之间。泛型函数的语法格式如下:

    • fun <T,S>函数名(形参列表):返回值类型{
      //函数体...
      }

    把上面泛型函数的语法格式和普通函数的语法格式进行对比,不难发现泛型函数的函数签名比普通函数的函数签名多了泛型声明,函数形参声明以尖括号括起来,多个函数形参之间以逗号(,)隔开,所有的函数形参声明都放在 fun关键字和函数名之间。
    例如,如下程序示范了泛型函数的用法。

    fun <T> copy(from:List<T>,to:MutableList<in T>){
        for (ele in from){
            to.add(ele)
        }
    }
    
    
    fun main(args: Array<String>) {
        var strList = listOf("ss","ddd")
        var objList:MutableList<Any> = mutableListOf(1,2,"ss")
        //指定泛型函数的 T为 String类型
        copy<String>(strList , objList)
        println(objList)
        var intList = listOf (7, 13, 17, 19)
        //不显式指定泛型函数的 T 的类型,系统推断出 T 为 Int 类型
        copy (intList , objList)
        println(objList)
    }
    

    上面代码在 fun 和 copy 函数名之间声明了泛型:<T>,这样即可在该函数的形参声明或返回值声明中使用 T 来代表类型 。
    声明了泛型函数之后,调用泛型函数时可以在函数名后用尖括号传入实际的类型,如上面代码所示:也可以在调用泛型函数时不为泛型参数指定实际的类型,而是让系统自动推断出泛型参数的类型。

    泛型函数也可用于扩展函数:

    //为泛型形参 T 扩展方法
    fun <T> T.toBookString(): String {
        return "《${this.toString()}》"
    }
    
    fun main(args: Array<String>) {
        val a = 2
        //显式指定泛型函数的 T 为 Int 类型
        println(a.toBookString())
        //不显式指定泛型函数的 T 的类型,系统推断出 T 为 Double 类型
        println(3.4.toBookString())
    }
    

    具体化类型参数

    Kotiin允许在内联函数(使用 inline修饰的函数)中使用 reified修饰泛型形参,这样即可将该泛型形参变成一个具体化的类型参数。 一旦将泛型形参变成具体化的类型参数,接下来在该函数中就可以像使用普通类型一样使用该类型参数,包括使用 is、 as 等运算符。

    例如,我们要从某个 List 集合中查找第一个指定类型的元素,由于程序需要根据指定类型来查找数据,所以最容易想到的做法是,定义一个类型来作为参数 。

    val db = listOf("ss", "rrr", java.util.Date(), 1111)
    
    fun <T> findData(clazz: Class<T>): T? {
        for (ele in db) {
            if (clazz.isInstance(ele)) {
                @Suppress("UNCHECKED_CAST")
                return ele as? T
            }
    
        }
    
        return null
    }
    
    fun main(args: Array<String>) {
        println(findData(Integer::class.java))
        println(findData(java.lang.Double::class.java))
    }
    

    上面代码确实可以实现我们的需求,但是这种方式未免太不优雅了,因为我们知道泛型形参本身就是类型参数,当程序调用该函数时完全可通过泛型形参来传入类型参数,何必还要通过函数的参数来传入类型呢?

    此时就可考虑使用 reified 修饰内联函数的泛型形参,这样就可直接在函数中使用该类型形参,从而避免用户通过函数的参数来传入类型。例如

    //使用 reified 修饰泛型形参,使之成为具体化的类型参数
    inline fun <reified T> findData(): T? {
        for (ele in db) {
            if (ele is T) {
                return ele
            }
    
        }
    
        return null
    }
    
    fun main(args: Array<String>) {
        println (findData<Int> ()) 
        println(findData<Double>())
    }
    

    设定类型形参的上限

    Kotlin 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,
    用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。下面程序示范了这种用法。

    class Apple1<T : Number> {
        var col: T
    
        constructor(col: T) {
            this.col = col
    
        }
    }
    
    fun main(args: Array<String>) {
        //显式指定泛型函数的 T 是Int 类型
        var ai = Apple<Int>(2)
        //显式指定泛型函数 的 T 是 Double 类型
        var ad: Apple<Double> = Apple(3.3)
    }
    

    上面程序定义了一个 Apple 泛型类,该 Apple 类的类型形参的上限是 Number 类,这表明使用 Apple 类时为 T 形参传入的实际类型参数只能是 Number 或 Number 类的子类。

    相关文章

      网友评论

          本文标题:泛型

          本文链接:https://www.haomeiwen.com/subject/yvvnzftx.html