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

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

    JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。在这里我们将讨论类型擦除对Kotlin的实际影响,以及如何通过将函数声明为inline来解决局限性。可以声明一个inline函数,使其类型实参不被擦除。我们将详细讨论实化类型参数,并查看一些有用的例子。

1.运行时的泛型:类型检查和转换

    和Java一样,Kotlin的泛型在运行时也被擦除了,这意味着泛型类实例不会携带用于创建它的实参的信息。例如,如果你创建了一个List<String>并将一堆字符串放到其中,在运行时你只能看到它是一个List,不能识别出列表本打算包含的是哪种类型的元素。

    想想执行下面的代码时这两个列表会发生什么

     val list1: List<String> = listOf("a", "b")
     val list2:List<Int> = listOf(1,2,3)

    即使编译器看到的是两种完全不同类型的列表,在执行的时候它们看起来却完全一样。尽管如此,你通常可以确信List<String>只包含字符串,而List<Int>只包含整数。因为编译器知道类型实参,并确保每个列表中值存储正确类型的元素。

    接下来我们谈谈伴随着擦除类型信息的约束。因为类型实参没有被存储下来,你不能检查它们。例如,你不能判断一个列表是包含字符串的列表还是包含其他对象的列表。一般而言,在is检查中不可能使用类型实参中的类型。下面这样的代码不会编译:

if (list1 is List<String>){}

    尽管在运行时可以完全断定这个值是一个List,但你依然无法判断它是一个含有字符串的列表,还是含有人,或者含有其他什么:这些信息被擦除了。注意擦除泛型类型信息是有好处的:应用程序使用内存总量变小,因为要保存在内存中的类型信息更少。

    如前所述,Kotlin不允许使用没有指定类型实参的泛型类型。那么你可能知道检查一个值是否是列表,而不是set或者其他对象,可以使用特殊的星号投影语句来做检查:

if (list1 is List<*>){}

    实际上,泛型类型拥有的每个类型形参都需要一个*。现在,你可以认为它就是拥有位置类型实参的泛型类型。

    注意:在as和as?转换中仍然可以使用一般的泛型类型。但是,如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出“unchecked cast”的警告。这仅仅是一个警告,你仍然可以继续使用这个值,就当它拥有必要的类型。

    对泛型类型做类型转换

 fun printSum(c:Collection<*>){
      val intList=c as? List<Int>?:throw IllegalArgumentException("List is expected")
      LogS(intList.sum())
  }
  printSum(listOf(1,2,3))//6

    编译一切正常:编译器只是发出了一个警告,这意味着代码是合法的。如果在一个整型的列表或者set上调用printSum函数,这一切都会如预期发生:第一种情况会打印出元素之和,第二种情况则会抛出IllegalArgumentException。但如果你传递一个错误类型的值,运行时会得到一个ClassCastException:

printSum(listOf('a','b'))//Char cannot be cast to Number

    我们来讨论一下在字符串列表上调用printSum函数时抛出的异常。你得到的并不是IllegalArgmentException,因为你没有办法判断实参是不是一个List<Int>。因此类型转换成功,无论如何函数sum都会在这个列表上调用,在这个函数执行期间,异常抛出了。这是因为sum函数试着从列表中读取Number值然后把它们加在一起。把String当Number用的尝试会导致运行时的ClassCastException。

    注意,Kotlin编译器足够智能的,在编译期它已经知道相应的类型信息时,is检查是允许的。

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

    c是否拥有类型Lits<Int>的检查是可行的,因为在编译期就确定了集合包含的是整型数字。

    通常,Kotlin编译器会负责让你知道哪些先插是危险的,而哪些又是可行的。你要做的就是了解这些警告的含义并且了解哪些操作是安全的。

    如前所述,Kotlin有特殊的语法结构可以允许你在函数体中使用具体的类型实参,但只有inline函数可以。家下来我们就来看看这个特性。

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

    前面我们已经讨论过,Kotlin泛型在运行时会被擦除,这以意味着如果你有一个泛型类的实例,你无法弄清楚在这个实例创建时用的究竟是哪些类型实参。泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数体中你不能决定调用它的类型实参:    

fun <T> isA(value:Any)=value is T
//Cannot check for instance of erased type:T

    通常情况下都是这样吗,只有一种例外可以避免这些限制:内联函数。内联函数的类型形参能够被实例化,这意味着你可以再运行时引用实际的类型实参。

    如果用inline关键字标记一个函数,编译器会把每一次函数调用都换成函数实际代码实现。使用内联函数还可能提升性能,如果该函数使用了lambda实参:lambda的代码也会内联,所以不会创建任何匿名类。这一节会展示inline函数大显身手的另一种场景:它们的类型参数可以被实例化。

    如果你把前面的例子中的isA函数声明成inline并且用reified标记类型参数,你就能够用该函数检查value是不是T的实例。

inline fun <reified T> isA(value:Any)=value is T
LogS(isA<String>("abc"))//true

    接下来我们看看使用实化类型参数的一些稍微有意义的例子。一个实化类型参数能发挥作用的最简单的例子就是标准函数库函数filterIsInstance。这个函数接收一个集合,选择其中哪些指定类的实例,然后返回这些被选中的实例。

    使用标准函数库filterIsInstance

  val items= listOf("one",2,"three")
  LogS(items.filterIsInstance<String>())//true

    通过指定<String>作为函数的类型实参,你表明感兴趣的只是字符串。因此函数返回类型是List<String>。在这种情况下,类型实参在运行时是已知的,函数filterInInstance使用它来检查列表中的值是不是指定为改类型实参的实例。

3.使用实化类型参数代替类引用

    另一种实化类型参数的常见使用场景是为接收java.lang.Class类型参数的API构建适配器。一个这种API的例子是JDK中的ServiceLoader,它接收一个代表接口或抽象类的java.lang.Class,并返回实现了该接口的类的实例。现在我们看看如何利用实化类型参数更容易地调用这些API。

    通过下面的调用来使用标准的ServiceLoader Java API加载一个服务

val serviceImpl=ServiceLoader.load(Service::class.java)

    ::class.java的语法展现了如何获取java.lang.Class对应的Kotlin类。这和Java中的Service.class是完全相同的。

    现在让我们用带实化类型参数的函数重写这个例子:

val serviceImpl=loadService<Service>()

    代码是不是短了不少?要加载的服务类现在被指定成了loadService函数的类型实参。把一个类指定城类型实参要容易理解的多,因为它的代码比较用::class.java语法更短。

    下面,我们看看这个loadServcie函数是如何定义的:

inline fun  <reified T> loadService(){
     return ServiceLoader.load(T::class.java)
}

    这种用在普通类上的::class.java语法也可以同样用在实化类型参数上。使用这种语法会产生对应到指定为类型参数的类的java.lang.Class,你可以正常的使用它。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值