泛型

作者: 凌寒天下独自舞 | 来源:发表于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 类的子类。

相关文章

  • 泛型 & 注解 & Log4J日志组件

    掌握的知识 : 基本用法、泛型擦除、泛型类/泛型方法/泛型接口、泛型关键字、反射泛型(案例) 泛型 概述 : 泛型...

  • 【泛型】通配符与嵌套

    上一篇 【泛型】泛型的作用与定义 1 泛型分类 泛型可以分成泛型类、泛型方法和泛型接口 1.1 泛型类 一个泛型类...

  • 泛型的使用

    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法 泛型类 泛型接口 泛型通配符 泛型方法 静态方法与...

  • Java 泛型

    泛型类 例如 泛型接口 例如 泛型通配符 泛型方法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型上下边...

  • 探秘 Java 中的泛型(Generic)

    本文包括:JDK5之前集合对象使用问题泛型的出现泛型应用泛型典型应用自定义泛型——泛型方法自定义泛型——泛型类泛型...

  • Web笔记-基础加强

    泛型高级应用 自定义泛型方法 自定义泛型类 泛型通配符? 泛型的上下限 泛型的定义者和泛型的使用者 泛型的定义者:...

  • 重走安卓进阶路——泛型

    ps.原来的标题 为什么我们需要泛型? 泛型类、泛型接口和泛型方法(泛型类和泛型接口的定义与泛型方法辨析); 如何...

  • Kotlin泛型的高级特性(六)

    泛型的高级特性1、泛型实化2、泛型协变3、泛型逆变 泛型实化 在Java中(JDK1.5之后),泛型功能是通过泛型...

  • Java 19-5.1泛型

    泛型类定义泛型类可以规定传入对象 泛型类 和泛型方法 泛型接口 如果实现类也无法确定泛型 可以在继承类中确定泛型:

  • 【Swift】泛型常见使用

    1、Swift泛型4种 泛型函数泛型类型泛型协议泛型约束 2、泛型约束3种 继承约束:泛型类型 必须 是某个类的子...

网友评论

      本文标题:泛型

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