Kotlin 知识梳理(13) 运行时的泛型

1、本文概要

本文是对<<Kotlin in Action>>的学习笔记,若是须要运行相应的代码能够访问在线环境 try.kotlinlang.org,这部分的思惟导图为: java

2、运行时的泛型:擦除和实化类型参数

2.1 运行时的泛型

Java同样,Kotlin的泛型在运行时也被擦除了,这意味着 泛型类实例不会携带用于建立它的类型实参的信息api

例如,若是你建立了一个List<String>,在运行时你只能看到它是一个List,不能识别出列表本打算包含的是String类型的元素。安全

接下来咱们谈谈伴随着擦除类型信息的约束,由于类型实参String没有被存储下来,你不能检查它们。例如,你不能判断一个列表是一个包含字符串的列表仍是包含其它对象的列表,也就是说,在is检查中不可能使用类型实参中的类型,例如函数

fun main(args: Array<String>) {
    val authors = listOf("first", "second")
    if (authors is List<Int>) {}
}
复制代码

将会在编译时抛出下面的异常:性能

>> Cannot check for instance of erased type
复制代码

Kotlin不容许使用 没有指定类型实参的泛型类型,若是但愿检查一个值是不是列表,而不是set或者其它对象,可使用特殊的 星号投影 语法来作这个检查:学习

if (value is List<*>)
复制代码

实际上,泛型类型拥有的每一个类型形参都须要一个*,如今你能够认为它就是 拥有未知类型实参的泛型类型this

asas?转换中仍然可使用通常的泛型类型,可是若是该类 有正确的基础类型但类型实参是错误的,转换也不会失败,由于在运行时转换发生的时候类型实参是未知的。所以,这样的转换会致使编译器发出unchecked cast的警告,例以下面这段程序:spa

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

fun main(args: Array<String>) {
    //(1) 正常运行。
    printSum(listOf(1, 2, 3))
    //(2) as 检查是成功的,可是调用 intList.sum() 方法时会抛出异常。
    printSum(listOf("a", "b", "c"))
}
复制代码

(2)调用时,并不会抛出IllegalArgumentException异常,而是在调用sum函数时才发生,由于sum函数试着从列表中读取Number值而后把它们加在一块儿,把String当作Number使用的尝试会致使运行时的ClassCastExceptioncode

假如在编译期,Kotlin已经知道了相应的类型信息,那么is检查是容许的:cdn

fun printSum(c: Collection<Int>) {
    if (c is List<Int>) {
        println(c.sum())
    }
}

fun main(args: Array<String>) {
    printSum(listOf(1, 2, 3))
}
复制代码

c是否拥有类型List<Int>的检查是可行的,由于咱们将函数类型的形参类型声明为了Collection<Int>,所以编译期就肯定了集合包含的是整型数字。

不过,Kotlin有特殊的语法结构能够容许你 在函数体中使用具体的类型实参,但只有inline函数能够,接下来让咱们来看看这个特性。

2.2 声明带实化类型参数的函数

Kotlin泛型在运行时会被擦除,这意味着若是你有一个泛型类的实例,你没法弄清楚在这个实例建立时用的到底是哪些类型实参。泛型函数的实参类型也是这样,在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参。

//将会在编译时抛出 "Cannot check for instance of erased type : T" 的异常
fun <T> isA(value : Any) = value is T
复制代码

内联函数的类型形参可以被实化

只有一种例外能够避免这种限制:内联函数。内联函数的类型形参可以被实化,意味着你能够 在运行时引用实际的类型实参。前面咱们介绍过内联函数的两个优势:

  • 编译器会把每一次函数调用都替换成函数实际的代码实现
  • 若是该函数使用了lambdalambda的代码也会内联,因此不会建立匿名类

这里,咱们介绍它一个新的优势:对于泛型函数来讲,它们的类型参数能够被实化。咱们将方面的函数修改以下,声明为inline而且用reified标记类型参数,就能用该函数检查value是否是T的实例:

inline fun <reified T> isA(value: Any) = value is T fun main(args: Array<String>) {
    println(isA<String>("abc"))
    println(isA<String>(123))
}
复制代码

运行结果为:

>> true
>> false
复制代码

filterIsIntance函数能够接收一个集合,选择其中那些指定类的实例,而后返回这些被选中的实例:

fun main(args: Array<String>) {
    val items = listOf("one", 2, "three")
    println(items.filterIsInstance<String>())
}
复制代码

运行结果为:

[one, three]
复制代码

该函数的简化实现为:

inline fun <reified T> Iterable<*>.filterIsIntance() : List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
}
复制代码

为何实化只对内联函数有效

咱们之因此能够在inline函数中使用element is T这样的判断,而不能在普通的类或函数中执行的缘由是由于:编译器把 实现内联函数的字节码 插入每一次调用发生的地方,每次你 调用带实化类型参数的函数 时,编译器都知道此次特定调用中 用做类型实参的确切类型,所以,编译器能够生成 引用做为类型实参的具体类 的字节码。

由于生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生类型擦除。注意,带reified类型参数的inline函数不能在Java代码中调用,普通的内联函数能够像常规函数那样在Java中调用 - 它们能够被调用而不能被内联。带实化类型参数的函数须要额外的处理,来把类型实参的值替换到字节码中,因此它们必须永远是内联的,这样它们不可能用Java那样普通的方式调用。

2.3 使用实化类型参数代替类引用

另外一种实化类型参数的常见使用场景是接收java.lang.Class类型参数的API构建适配器。例如JDK中的ServiceLoader,它接收一个表明接口或抽象类的java.lang.Class,并返回实现了该接口的实例。

val serviceImpl = ServiceLoader.load(Service::class.java) 复制代码

::class.java的语法展示了如何获取java.lang.Class对应的Kotlin类,这和Java中的Service.Class是彻底等同的,如今咱们用 带实化类型参数的函数 重写这个例子:

val serviceImpl = loadService<String>()
复制代码

loadService的定义为以下,要加载的服务类 如今被指定成了loadService 函数的类型实参

inline fun <reified T> loadService() {
    //把 "T::class" 当成类型形参的类访问。
    return ServiceLoader.load(T::class.java) } 复制代码

这种用在普通类上的::class.java语法也能够一样用在实化类型参数上,使用这种语法会产生对应到指定为类型参数的类的java.lang.Class,你能够正常地使用它,最后咱们以一个startActivity的调用来结束本节的讨论:

inline fun <reified T : Activity> Context.startActivity {
    val intent = new Intent(this, T::class.java) startActivity(intent) } >> startActivity<DetailActivity>() 复制代码

2.4 实化类型参数的限制

咱们能够按下面的方式来使用实化类型参数

  • 用在类型检查和类型转换中:is!isasas?
  • 使用Kotlin反射API::class
  • 获取对应的java.lang.Class::class.java
  • 做为调用其它函数的类型实参

不能作下面的事情:

  • 建立指定为类型参数的类的实例
  • 调用类型参数类的伴生对象的方法
  • 调用 带实化类型参数函数 的时候使用 非实化类型形参做为类型实参
  • 把类、属性或者非内联函数的类型参数标记为reified,由于实化类型参数只能用在内联函数上,使用实化类型参数意味着函数和全部传给它的lambda都会被内联,若是内联函数使用lambda的方法致使lambda不能被内联,或者你不想lambda由于性能的关系被内联,可使用noinline修饰符。

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

变型的概念描述了拥有 相同基础类型不一样类型实参 的类型之间是如何关联的,例如List<String>List<Any>之间如何关联。

3.1 为何存在变型:给函数传递实参

假设你有一个接受List<Any>做为实参的函数,那么把List<String>类型的变量传递给这个函数是否安全呢?咱们来看下面两个例子:

  • 第一个例子
fun printContents(list: List<Any>) {
    println(list.joinToString())
}

fun main(args: Array<String>) {
    printContents(listOf("abc", "bac"))
}
复制代码

这上面的函数能够正常地工做,函数把每一个元素都看成Any对待,并且由于每一个字符都是Any,所以这是彻底安全的,运行结果为:

>> abc, bac
复制代码
  • 第二个例子,与以前不一样,它会修改列表:
fun addAnswer(list : MutableList<Any>) {
    list.add(42)
}

fun main(args: Array<String>) {
    val strings = mutableListOf("abc", "bac")
    addAnswer(strings)
}
复制代码

这里声明了一个类型为MutableList<String>的变量strings,而后尝试把它传递给一个接收MutableList<Any>的函数,编译器将不会经过调用。

所以,当咱们将一个字符串列表传递给指望Any对象的列表时,若是 函数添加或者替换了 列表中的元素(经过MutableList来推断)就是不安全的,由于这样会产生类型不一致的可能,不然它就是安全的。

3.2 类、类型和子类型

变量的类型 规定了 变量的可能值,有时候咱们会把类型和类当成一样的概念使用,但它们不同。

类、类型

非泛型类

对于非泛型类来讲,类的名称能够直接看成类型使用。例如,var x : String声明了一个能够保存String类的实例的变量,而var x : String?声明了它的可空类型版本,这意味着 一个Kotlin类均可以用于构造至少两种类型

泛型类

要获得一个合法的类型,须要首先获得一个泛型类,并用一个做为 类型实参的具体类型 替换泛型类的 类型形参

List是一个类而不是类型,下面列举出来的全部替代品都是合法的类型:List<Int>List<String?>List<List<String>>,每个 泛型类均可能生成潜在的无限数量的类型

子类型

子类型的含义为:

任什么时候候若是须要的是类型A的值,可以使用类型B的值当作A的值,类型B就称为类型A的子类型。

例如IntNumber的子类型,但Int不是String的子类型,这个定义还代表了任何类型均可以被认为是它本身的子类型。

超类型

超类型子类型 的反义词

若是AB的子类型,那么B就是A的超类型。

编译器在每一次给变量赋值或者给函数传递实参的时候都要作这项检查:

  • 只有 值的类型变量类型的子类型 时,才容许存储变量的值
  • 只有当 表达式的类型函数参数的类型的子类型 时,才容许把该表达式传给函数

子类、子类型

在简单状况下,子类和子类型本质上是同样的,例如Int类是Number的子类,所以Int类型是Number类型的子类型。

一个非空类型是它的可空版本的子类型,但它们都对应着同一个类,你始终可以在可空类型的变量中存储非空类型值。

当开始涉及泛型类时,子类型和子类之间的差别就显得格外重要。正如咱们上面见到的,MutableList<String>不是MutableList<Any>的子类型。

对于泛型类MutableList而言,不管AB是什么关系,MutableList<A>既不是MutableList<B>的子类型也不是它的超类型,它就被称为 在该类型参数上是不变型的

Java中的全部类都是不变型的。在前一节中,咱们见到了List类,对它来讲,子类型化规则不同,Kotlin中的List接口表示的是只读集合。若是AB的子类型,那么List<A>就是List<B>的子类型,这样的类或者接口被称为 协变的

3.3 协变:保留子类型化关系

一个协变类是一个泛型类,若是AB的子类型,那么Producer<A>就是Producer<B>的子类型,咱们说 子类型化被保留了

Kotlin中,要声明类在某个类型参数上是能够协变的,在该类型参数的名称前加上out关键字便可,下面例子就能够表达为:Producer类在类型参数T上是能够协变的。

interface Producer<out T> {
    fun produce() : T } 复制代码

将一个类的类型参数标记为协变的,在 该类型实参没有精确匹配到函数中定义的类型形参时,可让该类的值做为这些函数的实参传递,也能够做为这些函数的返回值

你不能把任何类都变成协变的,这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用 的可能性,要保证类型安全,你只能用在所谓的out位置,意味着这个类 只能生产类型T的值而不能消费它们

在类成员的声明中类型参数的使用分为inout位置,考虑这样一个类,它声明了一个类型参数T并包含了一个使用T的函数:

  • 若是函数把T当成返回类型,咱们说它在out位置,这种状况下,该函数生产类型为T的值
  • 若是T用做函数参数的类型,它就在in的位置,这样函数消费类型为T的值。

所以类型参数T上的关键字有两层含义:

  • 子类型化会被保留,即前面谈到的Producer<Cat>Producer<Animal>的子类型
  • T只能用在out位置

在构造方法的参数上使用 out

构造方法的参数既不在in位置,也再也不out位置,即便类型参数声明成了out,仍然能够在构造方法参数的声明中使用它。

class Herd<out T : Animal> (vararg animals : T) { ... }
复制代码

若是把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用,不能调用存在潜在危险的方法。构造方法不是那种在实例建立以后还能调用的方法,所以它不会有潜在的危险。

然而,若是你在构造方法的参数上使用了关键字varval,同时就会声明一个gettersetter,所以,对只读属性来讲,类型参数用在了out位置,而可变属性在outin位置都使用了它。

class Herd<T : Animal> (var leadAnimal : T, vararg animals : T) { ... }
复制代码

上面这个例子中,T不能用out标记,由于类包含属性leadAnimalsetter,它在in位置用到了T

位置规则只覆盖了类外部可见的 API

位置规则只覆盖了类外部可见的api,私有方法的参数既不在in位置,也不在out位置,变型规则只会防止外部使用者对类的误用,但不会对类本身的实现起做用。

class Herd<out T : Animal> (private var leadAnimal : T, vararg animals : T) { ... }
复制代码

如今能够安全地让HerdT上协变,由于属性leadAnimal被声明成了私有。

3.4 逆变:反转子类型化关系

逆变的概念能够当作是协变的镜像,对一个逆变类来讲,它的子类型化关系与用做类型实参的类的子类型化关系是相反的:若是BA的子类型,那么Consumer<A>就是Consumer<B>的子类型。

Comparator接口为例,这个接口定义了一个compare方法,用于比较两个指定的对象:

interface Comparator<in T> {
    fun compare(e1 : T, e2 : T) : Int { ... }
}
复制代码

这个接口方法只是消费类型为T的值,这说明T只在in位置使用,所以它的声明以前用了in关键字。

一个为特定类型的值定义的比较器显然能够比较该类型任意子类型的值,例如,若是有一个Comparator<Any>,能够用它比较任意具体类型的值。

val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
val strings : List<String> = ...
strings.sortedWith(anyComparator)
复制代码

sortedWith指望一个Comparator<String>,传给它一个能比较更通常的类型的比较器是安全的。若是你要在特定类型的对象上执行比较,可使用能处理该类型或者它的超类型的比较器。

这说明Comparator<Any>Comparator<String>的子类型,其中AnyString的超类型。不一样类型之间的子类型关系这些类型的比较器之间的子类型关系 截然相反。

in关键字的意思是,对应类型的值是传递进来给这个类的方法的,而且被这些方法消费。和协变的状况相似,约束类型参数的使用将致使特定的子类型化关系。

一个类能够在一个类型参数上协变,同时在另一个类型参数上逆变。Function接口就是一个经典的例子:

interface Function1<in P, out R> {
    operator fun invoke(p : P) : R } 复制代码

这意味着对这个函数类型的第一类型参数来讲,子类型化反转了,而对于第二个类型参数来讲,子类型化保留了。例如,你有一个高阶函数,该函数尝试对你全部的猫进行迭代,你能够把一个接收动物的lambda传递给它。

fun enumerate(f : (Cat) -> Number) { ... }
fun Animal.getIndex() : Int = ...

>> enumerate(Animal :: getIndex)
复制代码

更多文章,欢迎访问个人 Android 知识梳理系列:

相关文章
相关标签/搜索