本文是对<<Kotlin in Action>>
的学习笔记,若是须要运行相应的代码能够访问在线环境 try.kotlinlang.org,这部分的思惟导图为: java
和
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
在as
和as?
转换中仍然可使用通常的泛型类型,可是若是该类 有正确的基础类型但类型实参是错误的,转换也不会失败,由于在运行时转换发生的时候类型实参是未知的。所以,这样的转换会致使编译器发出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
使用的尝试会致使运行时的ClassCastException
。code
假如在编译期,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
函数能够,接下来让咱们来看看这个特性。
Kotlin
泛型在运行时会被擦除,这意味着若是你有一个泛型类的实例,你没法弄清楚在这个实例建立时用的到底是哪些类型实参。泛型函数的实参类型也是这样,在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参。
//将会在编译时抛出 "Cannot check for instance of erased type : T" 的异常
fun <T> isA(value : Any) = value is T
复制代码
只有一种例外能够避免这种限制:内联函数。内联函数的类型形参可以被实化,意味着你能够 在运行时引用实际的类型实参。前面咱们介绍过内联函数的两个优势:
lambda
,lambda
的代码也会内联,因此不会建立匿名类这里,咱们介绍它一个新的优势:对于泛型函数来讲,它们的类型参数能够被实化。咱们将方面的函数修改以下,声明为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
那样普通的方式调用。
另外一种实化类型参数的常见使用场景是接收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>() 复制代码
咱们能够按下面的方式来使用实化类型参数
is
、!is
、as
、as?
Kotlin
反射API
,::class
java.lang.Class
,::class.java
不能作下面的事情:
reified
,由于实化类型参数只能用在内联函数上,使用实化类型参数意味着函数和全部传给它的lambda
都会被内联,若是内联函数使用lambda
的方法致使lambda
不能被内联,或者你不想lambda
由于性能的关系被内联,可使用noinline
修饰符。变型的概念描述了拥有 相同基础类型 和 不一样类型实参 的类型之间是如何关联的,例如List<String>
和List<Any>
之间如何关联。
假设你有一个接受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
来推断)就是不安全的,由于这样会产生类型不一致的可能,不然它就是安全的。
变量的类型 规定了 变量的可能值,有时候咱们会把类型和类当成一样的概念使用,但它们不同。
对于非泛型类来讲,类的名称能够直接看成类型使用。例如,var x : String
声明了一个能够保存String
类的实例的变量,而var x : String?
声明了它的可空类型版本,这意味着 一个Kotlin
类均可以用于构造至少两种类型。
要获得一个合法的类型,须要首先获得一个泛型类,并用一个做为 类型实参的具体类型 替换泛型类的 类型形参。
List
是一个类而不是类型,下面列举出来的全部替代品都是合法的类型:List<Int>
、List<String?>
和List<List<String>>
,每个 泛型类均可能生成潜在的无限数量的类型。
子类型的含义为:
任什么时候候若是须要的是类型
A
的值,可以使用类型B
的值当作A
的值,类型B
就称为类型A
的子类型。
例如Int
是Number
的子类型,但Int
不是String
的子类型,这个定义还代表了任何类型均可以被认为是它本身的子类型。
超类型 是 子类型 的反义词
若是
A
是B
的子类型,那么B
就是A
的超类型。
编译器在每一次给变量赋值或者给函数传递实参的时候都要作这项检查:
在简单状况下,子类和子类型本质上是同样的,例如Int
类是Number
的子类,所以Int
类型是Number
类型的子类型。
一个非空类型是它的可空版本的子类型,但它们都对应着同一个类,你始终可以在可空类型的变量中存储非空类型值。
当开始涉及泛型类时,子类型和子类之间的差别就显得格外重要。正如咱们上面见到的,MutableList<String>
不是MutableList<Any>
的子类型。
对于泛型类
MutableList
而言,不管A
和B
是什么关系,MutableList<A>
既不是MutableList<B>
的子类型也不是它的超类型,它就被称为 在该类型参数上是不变型的。
Java
中的全部类都是不变型的。在前一节中,咱们见到了List
类,对它来讲,子类型化规则不同,Kotlin
中的List
接口表示的是只读集合。若是A
是B
的子类型,那么List<A>
就是List<B>
的子类型,这样的类或者接口被称为 协变的。
一个协变类是一个泛型类,若是
A
是B
的子类型,那么Producer<A>
就是Producer<B>
的子类型,咱们说 子类型化被保留了。
在Kotlin
中,要声明类在某个类型参数上是能够协变的,在该类型参数的名称前加上out
关键字便可,下面例子就能够表达为:Producer
类在类型参数T
上是能够协变的。
interface Producer<out T> {
fun produce() : T } 复制代码
将一个类的类型参数标记为协变的,在 该类型实参没有精确匹配到函数中定义的类型形参时,可让该类的值做为这些函数的实参传递,也能够做为这些函数的返回值。
你不能把任何类都变成协变的,这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用 的可能性,要保证类型安全,你只能用在所谓的out
位置,意味着这个类 只能生产类型T
的值而不能消费它们。
在类成员的声明中类型参数的使用分为in
和out
位置,考虑这样一个类,它声明了一个类型参数T
并包含了一个使用T
的函数:
T
当成返回类型,咱们说它在out
位置,这种状况下,该函数生产类型为T
的值T
用做函数参数的类型,它就在in
的位置,这样函数消费类型为T
的值。所以类型参数T
上的关键字有两层含义:
Producer<Cat>
是Producer<Animal>
的子类型T
只能用在out
位置构造方法的参数既不在in
位置,也再也不out
位置,即便类型参数声明成了out
,仍然能够在构造方法参数的声明中使用它。
class Herd<out T : Animal> (vararg animals : T) { ... }
复制代码
若是把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用,不能调用存在潜在危险的方法。构造方法不是那种在实例建立以后还能调用的方法,所以它不会有潜在的危险。
然而,若是你在构造方法的参数上使用了关键字var
和val
,同时就会声明一个getter
和setter
,所以,对只读属性来讲,类型参数用在了out
位置,而可变属性在out
和in
位置都使用了它。
class Herd<T : Animal> (var leadAnimal : T, vararg animals : T) { ... }
复制代码
上面这个例子中,T
不能用out
标记,由于类包含属性leadAnimal
的setter
,它在in
位置用到了T
。
位置规则只覆盖了类外部可见的api
,私有方法的参数既不在in
位置,也不在out
位置,变型规则只会防止外部使用者对类的误用,但不会对类本身的实现起做用。
class Herd<out T : Animal> (private var leadAnimal : T, vararg animals : T) { ... }
复制代码
如今能够安全地让Herd
在T
上协变,由于属性leadAnimal
被声明成了私有。
逆变的概念能够当作是协变的镜像,对一个逆变类来讲,它的子类型化关系与用做类型实参的类的子类型化关系是相反的:若是
B
是A
的子类型,那么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>
的子类型,其中Any
是String
的超类型。不一样类型之间的子类型关系 和 这些类型的比较器之间的子类型关系 截然相反。
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)
复制代码