Kotlin中声明和使用泛型类及泛型函数的基本概念和Java类似。
同时Kotlin引入了新的概念,比如实化类型参数和声明点变型。这些概念对于我们来说可能很新奇。
实化类型参数: 允许你在运行时的内联函数调用引用作为类型实参的具体类型(对普通的类和函数来说,这样行不通,因为类型实参在运行时会被擦除)。
声明点变型: 可以说明一个带类型参数的泛型类型,是否是另一个泛型类型的字类型或者超类型,它们的基础类型相同但类型参数不同。
例如,它能调节是否可以把List<Int>
类型的参数传给期望List<Any>
的函数。使用点变型在具体使用一个泛型类型时,做同样的事,达到和Java通配符一样的效果。
泛型类型参数
泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型,例如,如果有一个List类型的变量,弄清楚这个列表中可以存储哪些事物是很有意义的。类型形参可以准确清晰地进行描述,就像这样“这个变量保存了字符串列表”,而不是“这边变量保存了一个列表”。
List<String> class Map<K, V> Map<String, Person>
Kotlin的声明概念和Java没有什么不一样,Kotlin编译器也常常能推导出类型实参:
val authors = listOf("Dmitry", "Svetlana")
需要注意的是,和Java不同,Kotlin始终要求类型实参要么被显式地说明,要么能被编译器推导出来。
因为泛型是在1.5版本才引入到Java的,它必须保证和基于老版本Java编写的代码兼容。而Kotlin从一开始就有泛型,所以它不支持原生态类型,类型实参必须定义。
泛型函数
泛型函数有它自己的类型形参。这些类型形参在每次函数调用时都必须替换成具体的类型实参。如:
// 先声明类型形参
fun <T> List<T>.slice(indices: IntRange): List<T>
声明泛型类
和Java一样,Kotlin通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。
interface List<T> {
Operator fun get(index: Int): T
}
如果你的类继承了泛型类(或者实现了泛型接口),你就得为基础类型的泛型形参提供一个类型实参。它可以是具体的类型或者另外一个类型形参:
class StringList:List<String> {
override fun get(index: Int): String = ...
}
class ArrayList<T> : List<T> {
override fun get(index: Int): T = ...
}
类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。
以计算列表元素只和的函数为例。它可以用在List<Int>
和List<Double>
上,但不可以用在List<String>
这样的列表上。可以定义一个类型参数约束,说明sum的类型形参必须是数字,来表达这个限制。
在Java中,用的是关键字extends来表达一样的概念:
// Java
<T extends Number> T sum(List<T> list)
// Kotlin
fun <T :Number> List<T>.sum(): T
声明带类型参数约束的函数,这个函数的实参必须是可比较的元素。
fun <T: Comparable<T>> max(first: T, second: T): T {
return if (first > second) first else second
}
// 字符串按字母表顺序比较, String 类继承了Comparable<String>
>>> println(max("kotlin", "java"))
kotlin
为一个类型参数指定多个约束
fun <T> ensureTrailingPerid(seq: T) where T : CharSequence, T : Appendable {
if(!seq.endsWith('.')) {
seq.append('.')
}
}
>>> val helloWorld = StringBuilder("Hello World")
>>> ensureTrailingPeriod(helloWorld)
>>> println(helloWorld)
hello World.
这种情况下,可以说明作为类型实参的类型必须实现CharSequence和Appendable两个接口。这意味着改类型的值可以使用访问数据(endsWith)和修改数据(append)两种操作。
让类型形参非空
如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。事实上,没有指定上界的类型形参将会使用Any?这个默认的上界。看看下面这个例子:
// value是可空的,所以要用安全调用
class Processor<T> {
fun process(value: T) {
value?.hashCode()
}
}
如果你想保证替换类型形参的始终是非空类型,可以通过指定一个约束来实现。如果你出来可空性之外没有任何限制,可以使用Any代替默认的Any?作为上界:
class Processor<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
运行时的泛型:擦除和实化类型参数
你可能知道,JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实现的类型实参在运行时是不保留的。在本节中,我们讲讨论类型擦除对Kotlin的实际影响,以及如何通过将函数声明为inline来解决其局限性。可以声明一个inline函数,使其类型实参不被擦除(或者,按照Kotlin术语,称作实化)。我们将详细讨论实化类型参数,并查看一些有用的例子。
运行时的泛型:类型检查和转换
和Java一样,Kotlin的泛型在运行时也被擦除了。这意味着泛型类实力不会携带用于创建它的类型实参的信息。
因为类型实参没有被存储下来,你不能检查它们。例如,你不能判断一个列表是一个包含字符串的列表还是包含其它对象的列表。一般而言,在is检查中不可能使用类型实参中的类型。下面这样的代码不会编译:
>>> if (value is List<String>) {...}
ERROR: Cannot check for instance of erased type
尽管在运行时可以完全断定这个值是一个List,但你依然无法判断它是一个含有字符串的列表,还是罕有人,或者含有其他什么:这些信息被擦除了。注意擦除泛型类型信息是有好处的:应用程序使用的内存总量较小,因为要保存在内存中的类型信息更少。
如前所述,Kotlin不允许使用没有指定类型实参的泛型类型。那么你可能想知道如何检查一个值是否是列表,而不是set或者其他对象。可以使用特殊的星号投影语法来做这种检查:
if (value is List<*>) {...}
声明带实化类型参数的函数
前面我们已经讨论过,Kotlin泛型在运行时会被擦除,这意味着如果你有一个泛型类的实例,你无法弄清楚在这个实例创建时用的究竟是哪些类型实参。泛型函数类型实参也是这样。在调用泛型函数的时候,在函数中你不能决定调用它用的类型实参:
>>> fun <T> isA(value: Any) = value is T
Error: 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
>>> println(isA<String>("abc") )
true
>>> println(isA<String>(123))
false
接下来我们看看使用实化类型参数的一些稍微有意义的例子。一个实化类型参数能发挥作用的最简单的例子就是标准库函数filterIsInstance。这个函数接收一个集合,选择其中那些指定类的实例,然后返回这些被选中的实例。下面展示了这个函数的用法。
>>> val items = listOf("one", 2, ''three")
>>> println(items.filterIsInstance<String>())
[one, three]
通过指定<String>
作为函数的类型实参,你表明感兴趣的只是字符串。因此函数的返回类型是List。这种情况下,类型实参在运行时是已知的,函数filterIsInstance使用它来检查列表中的值是不是指定为该类型实参的类的实例。
下面是Kotlin标准函数库filterIsInstance声明的简化版本。
// reified 声明了类型参数不会在运行时被擦除
// 可以检查元素是不是指定为类型实参的类的实例
inline fun <reified T>
Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
}
为什么实化只对内联函数有效
这是什么原理?为什么在inline函数中允许这样写element is T
,而普通的类或函数却不行?
正如在8.2节中讨论的,编译器把实现内联函数的字节码插入每一次调用发生的地方。每次你调用带实化类型参数的函数时,编译器都知道这次特定调用中用作类型实参的确切类型。因此,编译器可以生成引用作为类型实参的具体类的字节码。实际上,对代码清单9.8中的filterIsInstance<String>
调用来说,生成的代码和下面这段代码是等价的:
for (element in this) {
if (element is String) {
destination.add(element)
}
}
因为生成的字节码引用了具体类,而不是类型参数,它不会被运行发生的类型参数擦除影响。
注意,带reified类型参数的inline函数不能在Java中调用-------他们可以被调用而不能被内联。带实化类型参数的函数需要额外的处理,来把类型实参的值替换到字节码中,所以它们必须永远是内联的。这样它们不可能用Java那样普通的方式调用。
一个内联函数可以有多个实例化类型参数,也可以同时拥有非实例化类型参数和实化类型参数。注意,filterIsInstance函数虽然被标记成inline,而它并不期望lambda作为实参。在8.2.4中,我们提到把函数标记成内联只有在一种情况下有性能优势,即函数拥有函数类型的形参并且对其对应的实参----lambda----和函数inline,这里这样做是为了能够使用实化类型参数。
为了保证良好的性能,你仍然需要跟踪了解标记为inline的函数的大小。如果函数变得庞大,最好把不依赖实化类型参数的代码抽取到单独的非内联函数中。
使用实化类型参数代替类引用
另一种实化类型参数常见使用场景是为接收java.lang.Class类型参数的API构建适配器。一个这种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<Service>()
代码是不是短了不少?要加载的服务类现在被指定成了loadService函数的类型参数。
inline fun <reified T> loadService() {
return ServiceLoader.load(T::class.java)
}
变型:泛型和子类型化
为什么存在变型:给函数传递实参
假设你有一个接收List<Any>
作为实参的函数。把List<String>
类型的变量传给这个函数 是否安全?毫无疑问,把一个字符串传给一个期望Any的函数是安全的,因为String类型继承了Any。但是当String和Any变成List接口类型实参之后,情况就没有那么简单了。
fun printContents(list: List<Any>) {
println(list.ToString())
}
>>> printContents(listOf("abc", "bac"))
adc, bac
看起来这里字符串列表可以正常工作。函数把每个元素都当成Any对待,而且因为每一个字符串都是Any,这是完全安全的。
现在来看另外一个函数,它会修改列表(因此它接收一个MutableList作为参数):
fun addAnswer(list: MutableList<Any>){
list.add(42)
}
如果把一个字符串列表传给这个函数,会有什么不好的事情发生吗?
>>> val strings = mutableListOf("abc", "bac")
>>> addAnswer(strings)
>>> println(strings.maxBy{ it.length })
ClassCastException:Integer cannot be cast to String
这个例子展示了当期望的是MutableList<Any>
的时候把一个Mutable<String>
当作实参传递是不安全的,Kotlin编译器正确地禁止了它。