泛型

2018-11-05  本文已影响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 类的子类。

上一篇下一篇

猜你喜欢

热点阅读