泛型
与Java相似,Kotlin中的类也具有类型参数,如:
class Box<T>(t: T) {
var value = t
}
一般而言,创建类的实例时,我们需要声明参数的类型,如:
val box: Box<Int> = Box<Int>(1)
但当参数类型可以从构造函数参数等途径推测时,在创建的过程中可以忽略类型参数:
val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box<Int>
型变Variance
Java的变量类型中,最为精妙的是通配符(wildcards)类型(详见 Java Generics FAQ)。 但是Kotlin中并不具备该类型,替而代之的是:声明设置差异(declaration-site variance)与类型推测。
首先,我们考虑一下Java中的通配符(wildcards)的意义。该问题在文档 Effective Java, Item 28: Use bounded wildcards to increase API flexibility中给出了详细的解释。
首先,Java中的泛型类型是不变的,即List<String>
并不是List<Object>
的子类型。
原因在于,如果List是可变的,并不会
优于Java数组。因为如下代码在编译后会产生运行时异常:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this!
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
因此,Java规定泛型类型不可变来保证运行时的安全。但这样规定也具有一些影响。如, Collection
接口中的addAll()
方法,该方法的签名应该是什么?直观地,我们这样定义:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
但随后,我们便不能实现以下肯定安全的事:
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
}
(更详细的解析参见Effective Java, Item 25: Prefer lists to arrays)
以上正是为什么addAll()
的方法签名如下的原因:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
通配符类型(wildcard)的声明 ? extends T
表明了该方法允许一类对象是 T
的子类型,而非必须得是 T
本身。
这意味着我们可以安全地从元素( T的子类集合中的元素)读取 T
,同时由于
我们并不知道 T
的子类型,所以不能写元素。
反过来,该限制可以让Collection<String>
表示为Collection<? extends Object>
的子类型。
简而言之,带extends限定(上限)的通配符类型(wildcard)使得类型是协变的(covariant)。
理解为什么这样做可以使得类型的表达更加简单的关键点在于:如果只能从集合中获取元素,那么就可以使用 String
集合,
从中读取Object
也没问题 。反过来,如果只能向集合中_放入_元素,就可以用
Object
集合并向其中放入String
:在Java中有List<? super String>
是List<Object>
的超类。
后面的情况被称为“抗变性”(contravariance),这种性质是的只可以调用方法时利用String为List<? super String>
的参数。
(例如,可以调用add(String)
或者set(int, String)
),或者
当调用函数返回List<T>
中的T
,你获取的并非一个String
而是一个 Object
。
Joshua Bloch成这类为只可以从Producers(生产者)处 读取的对象,以及只可向Consumers(消费者)处写的对象。他表示:“为了最大化地保证灵活性,在输入参数时使用通配符类型来代表生产者或者消费者”,同时他也提出了以下术语:
PECS 代表生产者扩展,消费者超类(roducer-Extends, Consumer-Super)。
注记:当使用一个生产者对象时,如List<? extends Foo>
,在该对象上不可调用 add()
或 set()
方法。但这不代表
该对象是不变的。例如,可以调用 clear()
方法移除列表里的所有元素,因为 clear()
方法
不含任何参数。通配符类型唯一保证的仅仅是类型安全。不可变性完全是另一个话题了。
声明处型变
假设有一个泛型接口Source<T>
,该接口中不存在将 T
作为参数的方法,只有返回值为 T
的方法:
// Java
interface Source<T> {
T nextT();
}
那么,利用Source<Object>
类型的对象向 Source<String>
实例中存入引用是极为安全的,因为不存在任何可以调用的消费者方法。但是Java并不知道这点,依旧禁止这样操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
为了修正这一点,我们需要声明对象的类型为Source<? extends Object>
,有一点无意义,因为我们可以像以前一下在该对象上调用所有相同的方法,所以复杂类型没有增加值。但是编译器并不可以理解。
在Kotlin中,我们有一种途径向编译器解释该表达,称之为:声明处型变:我们可以标注源的变量类型为T
来确保它仅从Source<T>
成员中返回(生产),并从不被消费。
为此,我们提供输出修改:
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
常规是:当一个类C
中类型为T
的变量被声明为输出,它将仅出现于类C
的成员输出-位置,反之使得C<Base>
可以安全地成为
C<Derived>
的超类。
简而言之,称类C
是参数T
中的协变的,或T
是一个协变的参数类型。
我们可以认为C
是T
的一个生产者,同时不是T
的消费者。
out修饰符叫做型变注解,同时由于它在参数类型位置被提供,所以我们讨论声明处型变。 与Java的使用处型变相反,类型使用通配符使得类型协变。
另外除了out,Kotlin又补充了一项型变注释:in。它是的变量类型反变:只可以被消费而不可以
被生产。反变类的一个很好的例子是 Comparable
:
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, we can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
我们认为in和out是自解释(他们早已成功地被应用于C#中), 所以上文的解释并非必须的,并且读者可以从
The Existential Transformation: Consumer in, Producer out! 中获取更加深入的理解:)。
类型预测
使用处型变:类型预测
声明变量类型T为out是极为方便的,并且在运用子类型的过程中也没有问题。是的,当该类可以被仅限于返回T
,但是如果不可以呢?
一个很好的例子是 Array:
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}
该类中的T
不仅不可以被co- 也不能被逆变。这造成了极大的不灵活性。考虑该方法:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
该方法试图从一个数组中copy元素到另一个数组。我们尝试着在实际中运用它:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3)
copy(ints, any) // Error: expects (Array<Any>, Array<Any>)
这里,我们陷入了一个类似的问题:Array<T>
中的T
是不变的,所以不论是Array<Int>
或Array<Any>
都不是另一个的子类型。为什么?因为copy操作可能是不安全的行为,例如,它可能尝试向来源写一个String,
如果我们真的将其转换为Int
数组,随后 ClassCastException
异常可能会被抛出。
那么,我们唯一需要确保的是copy()
不会执行任何不安全的操作。我们试图阻止它向来源写,我们可以:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
我们称该做法为类型预测:源
并不仅是一个数组,并且可以要是可预测的。我们仅可调用返回类型为
T
的方法,如上,我们只能调用get()
方法。这就是我们使用使用位置可变性而非Java中的Array<? extends Object>
的
更加明确简单的方法
同时,你也可以利用in预测输入类型:
fun fill(dest: Array<in String>, value: String) {
// ...
}
Array<in String>
比对于Java中的Array<? super String>
, 例如,你可以向fill()
方法传递一个 CharSequence
数组或者一个 Object
数组。
星-预测 Star-Projections
有时,你试图说你并不知道任何类型声明的方法,但是仍旧想安全地使用他。 这里的安全方法指我们需要对out-预测
Kotlin提供了所谓的星预测语法如下:
- For
Foo<out T>
, whereT
is a covariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
. It means that when theT
is unknown you can safely read values ofTUpper
fromFoo<*>
. - For
Foo<in T>
, whereT
is a contravariant type parameter,Foo<*>
is equivalent toFoo<in Nothing>
. It means there is nothing you can write toFoo<*>
in a safe way whenT
is unknown. - For
Foo<T>
, whereT
is an invariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
for reading values and toFoo<in Nothing>
for writing values.
If a generic type has several type parameters each of them can be projected independently.
For example, if the type is declared as interface Function<in T, out U>
we can imagine the following star-projections:
Function<*, String>
meansFunction<in Nothing, String>
;Function<Int, *>
meansFunction<Int, out Any?>
;Function<*, *>
meansFunction<in Nothing, out Any?>
.
注记:星预测很像Java中的raw类型,但是比raw类型更加安全。
泛型函数
不仅仅是类可以类型参数,函数也可以有。类型参数放置在函数名前:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // extension function
// ...
}
To call a generic function, specify the type arguments at the call site after the name of the function:
val l = singletonList<Int>(1)
泛型约束
集合的所有可能类型可以被给定的被约束的泛型约束参数类型替代。
上界
约束最常见的类型是上界相比较于Java中的extends关键字:
fun <T : Comparable<T>> sort(list: List<T>) {
// ...
}
在冒号之后被声明的是上界:代替T
的仅可为 Comparable<T>
的子类型。例如:
sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>
默认的上界(如果没有声明)是Any?
。只能有一个上界可以在尖括号中被声明。
如果相同的类型参数需要多个上界,我们需要分割符 where-子句,如:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}