泛型

主要内容

  1. 泛型函数和类的声明
  2. 类型擦除(erasure)和具现(reified)
  3. Declaration-site and use-site variance

kotlin的泛型类型并不属于类型系统上的实现,而是尽量往Java方向兼容。所以实现形式上很多概念是等价的。

Generic type parameters

泛型指允许你定义的类型有 泛型参数(type parameters) 。当一个该类型的实例被创建了,类型参数被替代为指定的类型称之为 type arguments

Kotlin的类型参数在编译器中可自动推断类型,

1
val authors = listOf("Dmitry", "Svetlana")

因为传递给函数listOf的值的类型都是字符串,编译器推断创建了一个List<String>值对象。但如果创建一个空的list,因为无法进行推断,所以此时必须显式指定类型。

1
2
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()

这种声明是等价的。

NOTE 和Java不同,Kotlin要求类型参数要么显式指定,要么由编译器推导。但泛型是从JDK1.5才出现,为了兼容旧的版本,它允许你使用泛型类型时,可以不指定类型参数,叫做——原生类型(raw type)。例如,Java中使用List时,可以不指定类型参数。但Kotlin不同于Java,最开始的版本就提供泛型支持,它是不支持raw type的,泛型参数必须显式指定。

Generic functions and properties

当你编写的函数包含List集合时,需要处理泛型参数以使得List对所有类型都生效,这种带有泛型参数的函数称为 泛型函数(generic function)

以标准库中的泛型函数slice为例,

Figure 9.1

该函数的类型参数T被用作接收类型和返回类型;都为List<T>。在调用这类函数时,需要显式指定类型参数。但绝大多数情况下可以不指定,由编译器推导即可。

1
2
3
4
5
>>> val letters = ('a'..'z').toList()
>>> println(letters.slice<Char>(0..2)) // Specifies the type argument explicitly
[a, b, c]
>>> println(letters.slice(10..13)) // The compiler infers that T is Char here.
[k, l, m, n]

这里的结果类型都是List<Chart>。编译器推导T替代为Char作为返回类型List<T>的表述。

上一章的高阶函数filter可以进行编译器推导,

1
2
3
4
5
6
val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>(/* ... */)

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>

>>> readers.filter{ it !in authors }

这里的it编译器推导为String,因为实际的接收类型是readers ,它的实际参数类型是List<String>

类型参数可以声明在classes、interfaces、top-level functions 或 external functions这些地方。也可以对扩展属性也使用相同的语法。例如,下面是返回最后一个元素的extension property。

1
2
3
4
5
val <T> List<T>.penultimate: T  // This generic extension property can be called on a list of any kind.
get() = this[size - 2]

>>> println(listOf(1, 2, 3, 4).penultimate) // The type parameter T is inferred to be Int in this invocation.
3

You can’t declare a generic non-extension property

常规的属性(non-extension)不能有类型参数。因为一个类不能同时存储不同的类型值,因此声明一个泛型非扩展属性没有任何意义。你可以尝试这样做,但编译器报告错误:

1
2
val <T> x: T = TODO()
ERROR: type parameter of a property must be used in its receiver type

Declaring generic classes

和Java一样,Kotlin可以使用尖括号来声明泛型类或接口,一旦声明泛型类后,就可以在该类的body内使用该泛型参数。

1
2
3
interface List<T> {  // The List interface defines a type parameter T.
operator fun get(index: Int): T // T can be used as a regular type in an interface or a class.
}

泛型类的继承也和Java一致,

1
2
3
4
5
class StringList: List<String> {  // This class implements List, providing a speific type argument: String
override fun get(index: Int): String = ... }

class ArrayList<T>: List<T> { // Now the generic type parameter T of ArrayList is a type argument of list
override fun get(index: Int): T = ... }

StringList继承泛型了,声明了具体的类型参数,不再是泛型类,而是具体类;ArrayList执行了泛型参数并传递给父类泛型参数,自身作为泛型类实现。

一个类也可以引用自身作为泛型参数,例如Comparable接口,

1
2
3
4
5
6
7
interface Comparable<T> {
fun compareTo(other: T): Int
}

class String: Comparable<String> {
override compareTo(other: String): Int = /*...*/
}

Type parameter constraints

Kotlin的类型约束用法和Java无异。只不过是写法上的区别。

要声明一个约束,需要在类型参数后带上冒号(😃,再跟着指定的边界类型,

Figure 9.2

等价于Java 的写法<T extends Number>。下面的函数调用是运行的,因为Kotlin中的Int实际上继承自Number

1
2
>>> println(listOf(1, 2, 3).sum())
6

一旦指定了类型参数T的边界,你可以直接使用它的上界类型定义的方法或属性:

1
2
3
4
5
6
fun <T: Number> oneHalf(value: T): Double {  // Specifies Number as the type parameter upper bound
return value.toDouble() / 2.0 // Invoke a method defined in the Number class
}

>>> println(oneHalf(3))
1.5

下面编写一个泛型函数查找两值中的最大值。你需要定义Comparable的上界才能进行比较。

1
2
3
4
5
6
fun <T: Comparable<T>> max(first: T, second: T): T {  // The arguments of this function must be comprable elements.
return if (first > second) first else second
}

>>> println(max("kotlin", "java")) // The strings are compared alphabetically.
kotlin

如果传入的参数类型没有继承Comprable,编译将不通过,

1
2
3
>>> println(max("kotlin", 42))
ERROR: Type parameter bound for T is not satisfied:
inferred type Any is not a subtype of Comparable<Any>

T的上界是一个泛型类型Comparable<T>。前面看到,String类继承了Comparable<String>,因此可以传入字符串类型作为参数。

记住,依据Kotlin的operator conventions的规约,first > second实际上被编译为first.compareTo(second) > 0

很少的情况下需要对同一个类型参数指定多个上界的,你需要稍微使用不同的语法。例如,

1
2
3
4
5
6
7
8
9
10
11
fun <T> ensureTrailingPeriod(seq: T)
where T: CharSequence, T: Appendable {
if(!seq.endsWith('.')) { // Calls an extension function defined for the CharSequence interface
seq.append('.') // Calls the method from the Appendable interface
}
}

>>> val helloWorld = StringBuilder("Hello World")
>>> ensureTrailingPeriod(helloWorld)
>>> println(helloWorld)
Hello World.

这个例子中,使用的泛型类型必须同时继承CharSequenceAppendable接口。意味着可以对其进行访问(endsWith)和修改(append)操作。

Making type parameters non-null

如果你声明一个泛型类或函数,任何类型参数,包括可空,可以被替换为它自身的类型参数。实际上,一个类型参数如果不指定上界的话,它的上界则为Any?。考虑如下例子:

1
2
3
4
5
class Processor<T> {
fun process(value: T) {
value?.hashCode() // "value" is nullable, so you have to use a safe call.
}
}

函数process的参数value是可空的,尽管T没有标记上问号。这种情况下Processor类可以传入可用类型来替换T

1
2
val nullableStringProcessor = Processor<String?>()  // String?, which is a nullable type, is substituted for T.
nullableStringProcessor.process(null) // This code compiles fine, having "null" as the "value" argument.

如果你想要确保一个非空类型总是被替换使用。你可以实现这种特定的边界。否则就不指定,使用默认的Any?实现,

1
2
3
4
5
class Processor<T: Any> {  // Specifying a non-"null" upper bound
fun process(value: T) {
value.hashCode() // "value" of type T is now non-"null"
}
}

<T: Any>约束确保了T类型总是不为空的参数类型。传入可空参数将发生编译错误:

1
2
>>> val nullableStringProcessor = Processor<String?>()
Error: Type argument is not within its bounds: should be subtype of 'Any'

目前为止,Kotlin的基本泛型语法和Java类似。下面讨论运行时泛型的行为。

Generics at runtime: erased and reified type parameters

你可能已经知道,JVM上的泛型通过 类型擦除(type erasure) 实现,意味着一个泛型类的一个实例的类型参数在运行时是不保留的。我们将讨论Kotlin的类型确保的实际意义,以及如何应对inline函数的限制。你可以声明一个inline函数,这样它的类型参数不会被擦除erased(或,在Kotlin术语中,叫具现化reified)。

Generics at runtime: type checks and casts

和Java一样,Kotlin的泛型也是运行时擦除的。意味着一个泛型类的实例不会携带创建时的类型参数信息。例如,如果你创建了一个List<String>,里面放了一堆字符串,在运行时你将只能看到它是一个List。没有任何可能区分到它的实际包含的元素类型(当然,你可以获取到它的元素,校验它的类型,但不会给你任何保障,因为其它元素可能有不同类型)。

考虑如下代码并运行,

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

Figure 9.3

尽管编译器看出来这两个list有不同的类型,但在执行期间它们实际上是一样的。尽管如此,你可以确定List<String>仅包含字符串;List<Int>仅包含整型,因为编译器知道类型参数并且确保元素是被正确存储的。

我们继续讨论类型擦除的约束信息。因为类型参数不会被存储,你不能校验它们——例如,你不能校验一个list是一个字符串list而不是其它呢。通常规定,你无法使用is关键字来校验类型参数。下面代码不被编译:

1
2
>>> if (value is List<String>) { ... }
ERROR: Cannot check for instance of erased type

尽管在运行时你可以找出该值是一个List,却没有告知它到底是字符串list还是其它类型的list:类型信息被擦除erased了。泛型类型信息被擦除是有好处的:总体的内存信息将被减少,因为需要尽可能少的存储类型信息到内存中。

Kotlin不允许你使用没有指定泛型参数的泛型类型。这样你可能想知道如何校验list的值,你可能会用到特定的 star projection 语法:

1
if (value is List<*>) { ... }

显著地,引入 * 表述它会是任何类型。我们可以认为它表示的是未知类型(类似于Java的 List<?>)。这样一来,你就可以校验value是否是一个List,但仍然无法得知它的元素类型。

你仍然可以使用传统的asas?进行转换。但当该类包含有正确的基础类型和错误的参数类型,类型转换不会失败,因为进行转换时并不知道具体的类型参数。正因为这样,编译器会发出"unchecked cast"这样的警告。但仅仅是警告,后面你还可以继续使用它。

1
2
3
4
5
6
7
8
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // Warning here. Unchecked cast: List<*> to List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}

>>> printSum(listOf(1, 2, 3)) // Everything works as expected.
6

一切顺利:编译器仅产生一个告警,意味着代码是合理的(legitimate)。如果你在一个整型的list或set上调用printSum,第一种情况工作,后面会抛出异常。如果传递错误的值类型,则抛出ClassCastException运行期异常:

1
2
3
4
>>> printSum(setOf(1, 2, 3))  // Set isn't a list, so an exception is thrown.
IllegalArgumentException: List is expected
>>> printSum(listOf("a", "b", "c")) // The cast succeeds, and another exception is thrown later.
ClassCastException: String cannot be cast to Number

Kotlin的智能转换可以在编译期校验它的类型信息,

1
2
3
4
5
6
7
8
fun printSum(c: Collection<Int>) {
if (c is List<Int>) // This check is legitimate.
println(c.sum())
}
}

>>> printSum(listOf(1, 2, 3))
6

这里可以校验c是否是List<Int>,因为编译期已经确定了该集合类型。

通常,Kotlin编译期会处理好哪些类型是危险的(禁止is校验和抛出as转换的告警)。你仅需要知道告警的含义并理解操作是否是安全的。

前面提及到Kotlin有办法可以指定类型参数,但仅允许在inline内联函数使用。下面看看是怎么做到的。

Declaring functions with reified type parameters

前面提到,Kotlin的泛型和Java一样,都是运行期擦除的,意味着如果一个泛型类的实例,在实例被创建时无法得知它的类型参数。泛型函数也一样。当你调用一个泛型函数时,无法在调用时决定body的类型参数:

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

通常这是正确的,但有一种情况可以避免这种限制:内联函数。内联函数的类型参数可以具现化,意味着你可以引用到运行期的真实的类型参数。

Kotlin的内联函数,在编译期是被替换为真实代码实现的。标记一个inline函数可以提供性能,如果该函数使用lambda表达式作为入参的话:lambda代码被内联,匿名类被创建。这里显式inline函数另一个有用的场景:类型参数可以被具现(reified)。

更改前面的函数isA,标记上inline后,类型参数作为reified,现在你可以校验value的实例类型T

1
2
3
4
5
6
inline fun <reified T> isA(value: Any) = value is T  // Now this code compiles.

>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false

看看一些其它很少用到的具现化的例子。

1
2
3
>>> val items = listOf("one", 2, "three")
>>> println(items.filterIsInstance<String>())
[one, threee]

这里指定了<String>作为类型参数。因此它返回List<String>。这种情况下,the type argument is known at runtimefilterIsInstance被用来校验list实例的具体类型。

下面是它的标准库实现。

1
2
3
4
5
6
7
8
9
10
inline fun <reified T>  // "reified" declares that this type parameter will not be erased at runtime.
Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) { // You can check whether the element is an instance of the class sepcified as a type argument.
destination.add(element)
}
}
return destination
}

Why reification works for inline functions only

编辑器将字节码插入到对应的内联函数的调用部分。每次调用带有reified 类型参数的函数,编译器都知道它的真实类型。因此编译器可以生成相应特定类型参数对应类的字节码。实际上,filterIsInstance<String>调用的等价字节码如下:

1
2
3
4
5
for (element in this) {
if (element is String) { // References a specific class.
destination.add(element)
}
}

因为生成的字节码引用一个特定的类,而不是类型参数,它不会在运行时收到类型擦除的影响。

注意带有reified类型参数的inline函数不能被Java所调用。常规的内联函数可以被Java作为普通函数访问——但不是内联的。带有reified类型参数的内联函数要求额外的字节码替换操作,因为它必须是内联的。所以无法从普通的方式调用它,Java代码也一样。

内联函数可以有多个具现化参数类型,也可以没有。因为内联函数带来性能提供的唯一前提是函数的入参是一个lambda表达式,但这里的filterIsInstance没有任何入参。这种情况下,inline函数并不是作为性能提升的原因;相反,用于类型参数具现化。

为了确保良好的性能,你仍然需要关注inline函数编译后的内存占用大小。如果函数变得太大,最好重构提取出不依赖reified类型参数的非内联部分函数。

Replacing class references with reified type parameters

具现化类型参数的一个常规用法是为java.lang.Class构建API适配器。例如来自JDK的API是ServiceLoader,接收一个java.lang.Class作为接口或抽象类作为参数并返回该类的一个实例。我们看看具现化类型参数如何调用实现。

要使用Java API类ServiceLoader,可以如下调用:

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

::class.java语法显式了如何获得一个java.lang.Class对应的Kotlin类。实际上等价于Java的Service.class

我们可以使用具现化内联函数重写这个例子。

1
val serviceImpl = loadService<Service>()

更简短。现在该类不作为入参而是作为类型参数使用。下面重定义loadService内联函数:

1
2
3
inline fun <reified T> loadService() {  // The type parameter is marked as "refied".
return ServiceLoader.load(T::class.java) // Accesses the class of the type parameter as T::class
}

你仍然可以使用同样的::class.java语法在具现化类型参数上。因为它是运行期替换的。

Simplifying the startActivity function on Android

如果你是Android开发者,你可能发现另外一个更相似的例子:showing activities. 取代java.lang.Class进行传递,你可以使用具现化类型参数:

1
2
3
4
5
6
7
inline fun <reified T : Activity>
Context.startActivity() { // The type parameter is marked as "reified".
val intent = Intent(this, T::class.java) // Accesses the class of the type parameter as T::class
startActivity(intent)
}

startActivity<DetailActivity>() // Invokes the method to show an activity

Restrictions on reified type parameters

尽管具现化类型参数非常有用,但它有某些限制。某些继承了这些概念,其它的特性则取决于当前的实现又获取在将来的Kotlin版本不加约束。

更具体来说,下面是如何使用一个具现化类型参数:

  • In type checks and casts(is!isasas?)
  • To use the Kotlin reflection APIs(::class)
  • To get the corresponding java.lang.Class(::class.java)
  • As a type argument to call other functions

不可以 如下做:

  • Create new instances of the class specified as a type parameter
  • Call methods on the companion object of the type parameter class
  • Use a non-reified type parameter as a type argument when calling a function with reified type parameter
  • Mark type parameters of classes, properties, or non-inline functions as reified

最后一个约束导致一个有趣的结果:因为具现化类型参数仅能被用于内联函数,使用一个具现化类型参数意味着函数连着所有传入的lambda都是被内联的。如果由于使用方式导致lambda不被内联,或者由于性能的原因不希望其被内联,你可以使用noinline修改器比较不让其内联。

Variance: generics and subtyping

协变(variance) 的概念描述同一个基础类型在不同类型参数如何关联:例如,List<String>List<Any>。首先我们将描述为什么这种关联关系的重要性,以及在Kotlin中如何表述。理解协变是编写泛型类或函数的本质:它帮助你创建API不强制用户以不方便的方式打破他们的类型安全期望。

Why variance exists: passing an argument to a function

假设你有一个函数接收一个List<Any>作为参数。传递List<String>类型变量给该函数是安全的吗?当然是安全的,因为String类继承自Any。但当AnyString变为List接口的类型参数时,语义不再清晰。

例如,考虑一个函数打印内容到控制台。

1
2
3
4
5
6
fun printContents(list: List<Any>) {
println(list.joinToString())
}

>>> printContents(listOf("abc", "bac"))
abc, bac

看起来字符串可以工作。该函数把每个元素看做是Any,然后每个字符串都是Any,它是安全的。

现在看看另一个函数,它将修改list(所以用到MutableList作为参数)。

1
2
3
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}

如果传递字符串list会怎样?

1
2
3
4
>>> val strings = mutableListOf("abc", "bac")
>>> addAnswer(strings) // If this line compiled...
>>> println(strings.maxBy { it.length }) // ...you'd get an exception at runtime.
ClassCastException: Integer cannot be cast to String

你声明一个string的类型MutableList<String>变量。然后传递给该函数。如果编译器接收它,你便可以在该list添加一个整数,当尝试访问这个list的内容这将导致运行时异常。正因如此,该调用无法编译。这个例子展示了传递一个MutableList<String>作为参数但却受到一个MutableList<Any>是不安全的;Kotlin编译器禁止这样做。

由于类型的不一致性,添加或替换list中的元素是不安全的。其它行为则是安全的。在Kotlin中,如果接口正确可以很好控制。

Classes, types, and subtypes

某些情况下typeclass是等价的,某些情况下不是。

在非泛型类中,类名可以直接用作类型。例如,var x: String直接声明的变量的类型就是String类,并且也可以声明为可空类型,var x: String?

在泛型类中,要获得有效的类型,需要在运行时替换真实的类型,譬如List不是一个类型(它是一个类),但下列的情况是有效的类型:List<Int>List<String?>List<List<String>>,等等。每个泛型类产生一个可能的无限类型。

为了让我们讨论类型直接的关系,我们需要熟悉属于 subtype 。它的对立是 supertype 。子类型和父类型的关系有如下:

Figure 9.4

为什么一个类型到底是子类型或其它类型如此重要?编译器每次都对传递给函数的变量进行检查。考虑如下代码:

1
2
3
4
5
6
fun test(i: Int) {
val n: Number = i // Compiles, because Int is a subtype of Number

fun f(s: String) { /*..*/ }
f(i) // Doesn't compile, because Int isn't a subtype of String
}

将一个值存储在变量仅当该值的类型是该变量的子类型;例如,这里的i的类型是IntIntNumber的子类型,它可以存储在n变量中;但它不是String的子类型,编译将不通过。

某些情况下,子类等同于子类型的特性。例如,IntNumber的子类,因此Int类型是Number类型的子类型。

Figure 9.5

非空类型是可空类型的子类型,反之则不是,因为没有包含关系。

1
2
val s: String = "abc"
val t: String? = s // This assignment is legal because String is a subtype of String?.

诸如MutableList这样的泛型类,它的类型参数是不可变的,对于两个不同的类型A和B,MutableList<A>不是MutableList<B>的子类型或父类型。在Java中,所有类型都是不可变的(invariant)。

Covariance: preserved subtyping relation

泛型类的协变类遵循下面规则:Producer<A>Producer<B>的子类型当AB的子类型。它是 子类型保留的(subtyping is preserved) 。例如,Producer<Cat>Producer<Animal>的子类型,因为CatAnimal的子类型。

在Kotlin 中声明一个类成为协变类,使用out关键字在类型参数前:

1
2
3
interface Producer<out T> { // This class is declared as covariant on T.
fun produce(): T
}

使一个类的类型参数协变,可以令参数为该类的函数,入参可以不匹配函数定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
open class Animal {
fun feed() { ... }
}

class Herd<out T: Animal> { // The type parameter is now covariant.
val size: Int get() = ...
operator fun get(i: Int): T { ... }
}

fun feedAll(animals: Herd<Animal>) {
for (i in 0 until animals.size) {
animals[i].feed()
}
}

fun takeCareOfCats(cats: Herd<Cat>) {
for(i in 0 until cats.size) {
cats[i].cleanLitter()
}
feedAll(cats) // You don't need a cast.
}

你不能令任何类都是协变的:会导致不安全。

假设一个类声明了类型参数T并且包含一个函数使用了T。我们说T作为函数的返回类型时,是在out位置。如果T作为函数的一个参数,是在in位置。

Figure 9.6

out位置要求所有用到的类型参数所对应的T仅能在声明了out的类型关联使用。例如,Hered类使用了T就在out的位置。

1
2
3
4
class Herd<out T: Animal> {
val size: Int get() = ...
operator fun get(i: Int): T { ... } // Uses T as the return type
}

out关键字在T类型参数意味着:

  • 子类型是被保留的(Producer<Cat>Producer<Animal>的子类型)
  • T仅能被用于out位置。

现在在看下List<T>接口。Kotlin的List是只读的,它只有一个get方法返回T类型的元素,它是协变的。

1
2
3
4
interface List<out T>: Collection<T> {
operator fun get(index: Int): T // Read-only interface that defines only methods that return T (so T is in the "out" position
// ...
}

注意类型参数不仅是作为参数的类型或返回类型,也可以作为其它类型的类型参数。例如,List接口包含方法subList返回List<T>

1
2
3
4
interface List<out T>: Collection<T> {
fun subList(fromIndex: Int, toIndex: Int): List<T> // Here T is in the "out" position as well.
// ...
}

这里的T用在subList函数的out位置。

但你不能声明MutableList<T>作为协变类,因为它包含方法接收T作为参数并返回该值(T同时出现在inout位置)。

1
2
3
4
interface MutableList<T>  // MutableList can't be declared a covariant on T ...
: List<T>, MutableCollection<T> {
override fun add(element: T): Boolean // ...because T is used in the "in" postion.
}

编译器严格执行该约束。如果该类被声明为一个协变会报错误:Type parameter T is declared as 'out' but occurs in 'in' postion.

注意构造参数既没有in也没有out。即使一个类型参数被声明为out,你仍然可以使用构造参数的声明:

1
class Herd<out T: Animal>(vararge animals: T) { ... }

构造函数不是方法,它在实例被创建后调用,因此不会有潜在的隐患。

你可以在构造参数使用valvar关键字,也可以声明一个getter和setter(mutable)。因此,类型参数用在out位置作为只读属性,同时在outin位置则作为一个可变(mutable)属性:

1
class Herd<T: Animal>(var leadAnimal: T, vararge animals: T) { ... }

现在可以令HerdT协变,因为leadAnimal属性是private的。

Contravariance: reversed subtyping relation

逆变(contravariance) 可以认为是协变(covariance)的镜像:对于逆变类,子类型关联的是它的类型参数的子类型的反面。我们以一个例子开始:Comparator接口。该接口定义一个方法,compare,比较两个对象:

1
2
3
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int { .. } // Uses T in "in" positions
}

可以看到该接口的唯一方法仅消费T类型。意味着T仅被用于in位置,因此它的声明前面是in关键字。

定义了comparator的类型值,可以对该类型的任意子类型进行比较。

1
2
3
4
5
>>> val anyComparator = Comparator<Any> {
... e1, e2 -> e1.hashCode() - e2.hashCode()
... }
>>> val strings: List<String> = ...
>>> strings.sortedWith(anyComparator) // You can use the comparator for any objects to compare specific objects, such as strings.

意味着Comparator<Any>Comparator<String>的子类型,然而AnyString的超类型。两种类型的子类化关系刚好是相反的。

现在对逆变准备的更充分的定义。一个类如果是逆变的,那么它是一个泛型类(以Consumer<T>为例),它的泛型参数遵循如下:如果BA的超类型,那么Consumer<A>Consumer<B>的子类型。类型参数AB交换位置,所以我们看到子类化是反过来的。例如,Consumer<Animal>Consumer<Cat>的子类型。

下图显示了协变和逆变的类型参数子类化的不同比较。可以看到,ProducerConsumer的子类化关系是相反的。

Figure 9.7

in关键字表示相应类型的值被 passed in 到该类的方法消费。类似于协变,对类型参数的约束产生特定的子类化关系。类型T带有in关键字意味着子类化被反转,T仅能被用于in位置。下标总结了可能的variance的情况。

Covariant Contravariant Invariant
Producer<out T> Consumer<in T> MutableList<T>
Subtyping for the class is preserved: Producer<cat> is a subtype of Producer<Animal> Subtyping is reversed: Consumer<Animal> is a subtype of Consumer<Cat> No subtyping
T only in out positions T only in in positions T in any position

一个类或接口同时可以对一个类型参数协变(covariant),对另一个类型逆变(contravariant)。最经典的例子莫过于Function接口,下面是一个类型参数的Function定义:

1
2
3
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}

上面使用到了operator规约,说明它有更简单的写法(P) -> R。这里看到P标记为in它是作为入参,而R则作为返回类型,用out标记仅能用在out的位置。意味着对于第一个类型参数P是逆变的、反转的;而对于第二个类型参数R是保留的、协变的。

1
2
3
4
fun enumerateCats(f: (Cat) -> Number) { ... }
fun Animal.getIndex(): Int = ...

>>> enumerateCats(Animal::getIndex) // This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype Of Number.

下图为其subtyping relationship。

Figure 9.8

Use-site variance: specifying variance for type occurrences

在类的声明上指定其可变性的能力会变得很方便,因为修改会作用到该类的使用的任何地方。这叫做 declaration-site variance 。如果你熟悉Java的通配符类型(? extends? super),你会意识到Java处理可变性的差异。在Java中,每次你都可以使用子类型或超类型来替代。这叫做 use-site variance

Declaration-site variance in Kotlin vs. Java wildcards

Declaration-site variance 的方式会使得代码更加简明扼要,因为可变类仅修改一次,其它所有用到的地方都不用考虑。在Java,API的创建行为依据用户的期望,标准库总是使用通配符:Function<? super T, ? extends R>。Java8标准库到处都用到Function接口,例如Stream.map方法的声明如下,

1
2
3
public interface Stream<T> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

声明地方对可变类型的一次修改即生效可使的代码更加简明和优雅。

Kotlin 支持use-site variance,允许指定当前的类型参数。

1
2
3
4
5
6
fun <T> copyData(source: MutableList<T>,
destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}

该函数将元素从一个集合copy到另一个集合。尽管两个集合都是不可变类型,但源集合仅仅用作读,目标集合仅用作写。这种情况,集合的元素类型并不需要匹配。你可以将一个字符串集合拷贝到一个Any类型的集合内。

为了便于比较,我们在该函数引入泛型参数,

1
2
3
4
5
6
7
8
9
10
11
12
fun <T: R, R> copyData(source: MutableList<T>,  // Source's element type should be a subtype of the destination's element type
destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}

>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems) // You can call this function, because Int is a subtype of Any.
>>> println(anyItems)
[1, 2, 3]

这里要求destination的元素的类型,需要是source的元素类型的父类型,否则无法调用add()

不过Kotlin提供了更加优雅的方式来表述这种问题。当实现的函数内仅仅进行out、或仅仅进行in位置处理时,你可以直接使用可变修改器(variance modifier)定义类型参数。如下,

1
2
3
4
5
6
fun <T> copyData(source: MutableList<out T>,  // You can add the "out" keyword to the type usage: no methods with T in the "in" postion are used.
destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}

你可以在一个类型声明的任意类型参数的使用上指定一个可变性修改器:参数类型、本地变量类型、函数返回类型等等。这里出现的称为 类型投影(type projection) :我们说source不是一个常规的MutableList,严格来说,是一个 投影(projected) 。你仅能调用那些返回泛型类型参数的方法,又或者,严格来说,仅能在out位置使用它。编译器禁止调用那些类型用作参数的方法(禁止调用 in 位置的方法):

1
2
3
>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add(element: E): Boolean'

Don’t be surprised that you can’t call some of the methods if you’re using a projected type 。如果你需要调用它们,你需要使用常规类型取代投影类型。这要求你声明第二个参数类型,依赖其投影类型。譬如上述代码的<T: R, R>

如果类型声明已经为out,在使用的使用再声明为协变是没意义的。譬如List<out T>,实际等同于List<T>,因为List的声明类本来就是class List<out T>。编译器会告警这样的投影是多余的。

类似的情况也出现在逆变类型上。我们可以使用in可变修改器表明该值是作为一个消费者,消费的类型将被反转(reversed)替换为父类型。如下,

1
2
3
4
5
6
fun <T> copyData(source: MutableList<T>,
destination: MutableList<in T>) { // Allows the destination element type to be a supertype of the source element type
for (item in source) {
destination.add(item)
}
}

注意:use-site variance的声明在Kotlin是跟Java的通配类型对应的。MutableList<out T>在Kotlin等同于Java的MutableList<? extends T>in投影MutableList<in T>等同于MutableList<? super T>

Star projection: using * instead of a type argument

讨论类型检查和转换的时候,曾提及过 star-projection 这一语法用来告知 no information about a generic argument 。就是无法知道类型参数的信息。例如List<*>,就是包含未知元素类型的集合。

首先注意,MutableList<*>不是MutableList<Any?>(这很重要因为这里的MutableList<T>是不可变的T)。一个MutableList<Any?>告诉了你它包含的元素是任意类型。然而,MutableList<*>包含的元素是一个特定的元素,但我不清楚它是什么。这种特定的元素类型可能是String,也可能是其它,这样写的代码仅包含该特定类型的元素。因为你不知道类型是什么,你不能把任意东西都放进去,这会侵犯原来类型的代码调用。但可以从list获取元素,因为你知道所有存储于该list的元素,它都是Any?的子类型,

1
2
3
4
5
6
7
>>> val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
>>> val chars = mutableListOf('a', 'b', 'c')
>>> val unknownElements: MutableList<*> = // MutableList<*> isn't the same as MutableList<Any?>.
if (Random().nextBoolean()) list else chars
>>> unknownElements.add(42) // The compiler forbids you to call this method.
Error: Out-projected type 'MutableList<*>' prohibits the use of 'fun add(element: E): Boolean'
>>> println(unknownElements.first()) // It's safe to get elements: first() returns an element of the Any? type.

为什么编译器吧MutableList<*>引述为一个out-project类型?在上下文,MutableList<*>是被投影到MutableList<out Any?>:你无法知道元素的类型,但对于获取Any?类型的元素是安全的,而对于向里面添加元素则显得不安全。说到Java的通配类型,MyType<*>在Kotlin是和Java的MyType<?>相对应的。

注意 对于逆变类型参数诸如Consumer<in T>,它的star-projection等价于<in Nothing>。实际上,你无法调用基于该投影的任何方法。如果类型是逆变的,它仅作为一个消费者,你不知道它实际上可以消费什么。因此,你不能让它消费。

当类型参数并不重要时,你可以使用 star-projection 的语法:在签名部分你不会用到类型参数的任何方法,或者你仅仅只读数据不关心它的具体类型。例如,你可以实现printFirst函数并接收List<*>作为参数:

1
2
3
4
5
6
7
8
fun printFirst(list: List<*>) {  // Every list is a possible argument.
if (list.isNotEmpty()) { // isNotEmpty() doesn't use the generic type parameter.
println(list.first()) // first() now returns Any?, but in this case that's enough.
}
}

>>> printFirst(listOf("Svetlana", "Dmitry"))
Svetlana

如果是作为use-site variance,需要引入泛型类型参数:

1
2
3
4
5
fun <T> printFirst(list: List<T>) {  // Again, every list is a possible argument.
if (list.isNotEmpty()) {
println(list.first()) // first() now returns a value of T.
}
}

带有星投影的语法更加简明,但它仅工作在你不需要关心实际泛型类型参数的情况:仅能使用产生该值的方法,不关心这些值的类型。

下面是另外一个星投影的例子,假设我们要验证用户输入,并声明FieldValidator。它包含的类型仅在in位置,因此它可以声明为逆变。

1
2
3
4
5
6
7
8
9
10
11
interface FieldValidator<in T> {  // Interface declared as contravariant on T
fun validate(input: T): Boolean // T is used only in the "in" position(this method consumes a value of T).
}

object DefaultStringValidator: FieldValidator<String> {
override fun validate(input: String) = input.isNotEmpty()
}

object DefaultIntValidator: FieldValidator<Int> {
override fun validate(input: Int) = input >= 0
}

想象你希望在相同的容器存储所有的validator,并根据输入类型获取正确的validator。最先你可能会用一个map来存储。并为任意类型存储validator,你可能需要声明一个KClass类型的map:

1
2
3
>>> val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
>>> validators[String::class] = DefaultStringValidator
>>> validators[Int::class] = DefaultIntValidator

一旦这样做了,当使用这些validator时会觉得有困难。你不能用FieldValidator<*>来校验字符串。这不是类型安全的,因为编译器不知道它是哪种validator:

1
2
>>> validators[String::class]!!.validate("")  // The value stored in the map has the type FieldValidator<*>.
Error: Out-projected type 'FieldValidator<*>' prohibits the use of 'fun validate(input: T): Boolean'

这类错误在以前介绍MutableList<*>描述过了。这类错误意味着输入特定类型到一个未知类型的入口,不是类型安全的。一种方式是,显式转换为指定的类型,但它既不是类型安全的、也不推荐这样做,不过下面可以列出代码作为参考一下:

1
2
>>> val stringValidator = validators[String::class] as FieldValidator<String>  // Warning: unchecked cast
>>> println(stringValidator.validate(""))

编译器发出告警关于unchecked 的转换。注意,这段代码仅会在校验时失败,不是在转换的地方,因为运行时所有泛型信息都被擦除。

1
2
3
4
>>> val stringValidator = validators[Int::class] as FieldValidator<String>  // You get an incorrect validator(may be by mistake), but this code compiles.
>>> stringValidator.validate("") // The real error is hidden until you use the validator.
java.lang.ClassCastException:
java.lang.String cannot be cast to java.lang.Number at DefaultIntValidator.validate

这段代码在编译期仅有一个告警,运行期由于泛型擦除,出现错误。

这种方案不是类型安全的(type-safe)、并且容易出错(error-prone)。因此,需要探讨一下其它可能的方案。

下面代码使用同一个validatorsmap,但封装了所有的访问到两个泛型方法,仅包含正确的validator的注册和返回。该代码对未校验的转换会发出一个告警,但这里的Validators控制所有的访问,并确保不可能错误修改map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object Validators {
private val validators = // Uses the same map as before, but now you can't access it outside
mutableMapOf<KClass<*>, FieldValidator<*>>()

fun <T: Any> registerValidator(kClass: KClass<T>, fieldValidator: FieldValidator<T>) {
validators[kClass] = fieldValidator // Puts into the map only the correct key-value pairs, when a validator corresponds to a class
}

@Suppress("UNCECKED_CAST") // Suppresses the warning about the unchecked cast to FieldValidator<T>
operator fun <T: Any> get(kClass: KClass<T>): FieldValidator<T> =
validators[kClass] as? FieldValidator<T> ?: throw IllegalArgumentException("No validator for ${kClass.simpleName}")
}

>>> Validators.registerValidator(String::class, DefaultStringValidator)
>>> Validators.registerValidator(Int::class, DefaultIntValidator)

>>> println(Validators[String::class].validate("Kotlin"))
true
>>> println(Validators[INt::class].validate(42))
true

现在你有了一个type-safe API。所有的unsafe逻辑被隐藏在类的body部分;通过本地化实现,确保了它不会被错误使用。编译器禁止你使用不正确的validator,因为Validators对象总是给定了正确的validator的实现:

1
2
>>> println(Validators[String::class].validate(42))  // Now the "get" method returns an instance of FieldValidator<String>.
Error: The integer literal does not conform to the expected type String

严格来说,这是一种设计模式,并不算是Kotlin的特性内容。实现这种模式可以非常容易地对自定义泛型类型进行扩展。将unsafe的代码进行本地化分离避免误用。

Java的泛型和协变性通常被认为是该语言最棘手的部分。在Kotlin中,在Kotlin中我们尽最大努力使其易于理解和易于使用,同时有保留对Java的互操作性。

Summary

  • Kotlin的泛型和Java的泛型非常相似:你可以以同样的方式声明泛型函数或类。
  • 在Java,泛型类型的类型参数(type parameter)仅存在于编译期。
  • 在使用类型时,不能同时将类型参数和is操作符一起使用,因为 类型参数是运行期擦除的
  • 类型参数和内联函数一起使用时可以标记上reified,它允许你在运行时操作is的校验以及获得java.lang.Class实例。
  • Variance is a way to specify whether one of two generic types with the same base class and different type arguments is a subtype or a supertype of the other one if one of the type arguments is the subtype of the other one.
  • 你可以通过声明在类型参数上的类作为协变(covariant),当且仅当该类型参数被用在out位置。
  • 逆变(contravariant)则是相反:声明的类的类型参数仅用在in位置。
  • 只读接口List在kotlin被声明为协变,意味着List<String>List<Any>的子类型。
  • Function类的接口声明就是第一个类型参数声明为逆变(contravariant),以及第二个类型参数声明为协变(covariant),可以使得(Animal) -> Int(Cat) -> Number的类型子类。
  • Kotlin可以让你同时对泛型类(declaration-site variance)和特定泛型类型(use-site variance)指定可变性(variance)。
  • 星投影(star-projection)语法被用于类型参数未知或不重要/不敏感的情况。