和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的类型系统中最棘手的部分之一是通配符类型。在Kotlin中则没有这个概念。取而代之的则是declaration-site variance and type projections
,后者姑且翻译为类型推断。
首先,我们来想想为什么Java需要这些神秘的通配符。这个问题在Effective Java
中被解释到:使用有界通配符来增加API的灵活性。首先,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>
}
(在Java中,我们艰难地学习了本课程:将集合转换为数组)
这就是为什么addAll()
的实际签名如下所示:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
通配符类型参数? extends E
表示此方法接受E的一些子类型的对象的集合,而不是E本身。这意味着我们可以安全地从集合中读取E(这个集合的元素是E的子类的实例),但不能写入它,因为我们不知道元素到底是E类型的哪个子类。针对该限制,我们渴望实现:Collection <String>
是Collection <? extends Object>
的子类。 用简明的话说,具有继承界限(上限)的通配符使得类型更协变(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谈起过只从生产者读,以及只写向消费者的对象。 他建议:“为了最大限度的灵活性,在代表生产者或消费者的输入参数上使用通配符”,并提出以下助记符:
PECS代表生产者继承,消费者父类( Producer-Extends, Consumer-Super)。
注意:如果你使用一个生产者对象,也就是说属于List<? extends Foo>
类型,你不允许调用该对象的add()
或set()
方法,但这并不意味着该对象是不可变的:例如,没有什么可以阻止你调用clear()
来从列表中删除所有的项目,因为clear()
根本没有任何参数。 通配符(或其他类型的差异)保证的唯一的事情是类型安全。 不变性则是一个完全不同的概念。
分歧声明点(Declaration-site variance)
假设我们有一个通用的接口Source <T>
,它没有任何方法可以将T作为参数,只有返回T的方法:
// Java
interface Source<T> {
T nextT();
}
然后,将Source <String>
的实例存储在Source <Object>
类型的变量中是绝对安全的 - 没有调用消费者的方法。 但Java不知道这一点,但仍然禁止:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
要解决这个问题,我们必须声明Source <? extends Object>
类型的对象,这是没有意义的,因为我们可以调用所有相同的方法在这样的变量以前,所以没有增加更复杂的类型的值。 但编译器不知道。
在Kotlin中,有一种方式可以向编译器解释这种事情。 这称为declaration-site variance
:我们可以注释Source的类型参数T,以确保它仅从Source <T>的成员返回(生产),并且不会消耗。 为此,我们提供out修饰符:
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被声明为out
时,它只能在C的成员的out位置出现,但返回的C <Base>
则是安全的C <Derived>
的父型。
在“聪明的话”中,他们说C类在参数T中是协变的,或者说是T是一个协变类型的参数。 你可以认为C是T的生产者,而不是T的消费者。
out修饰符被称为分歧注解(variance annotation),并且由于它在类型参数处出现,所以我们称之为分歧声明点(declaration-site variance)。 这与Java的分歧使用点(use-site variance)形成对照,其中类型用法中的通配符使类型协变。
除了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!
}
我们相信这些单词是自我解释的(因为它们已经在C#中成功使用了一段时间),因此上面提到的助记符并不是真正需要的,可以改写为更高的概括:
Consumer in, Producer out!
类型推断(Type projections)
使用分歧点:类型推断(Use-site variance: Type projections)
将类型参数T声明为out非常方便,并避免在使用处进行子类转换的麻烦,但是某些类实际上不能仅限于返回T! 一个很好的例子是Array
:
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}
该类不能使用类型参数T共同或相反。这种强加了一些不灵活性。考虑以下功能:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
该函数应该将条目从一个数组复制到另一个数组。 我们试着在实践中应用它:
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>
都不是另一个的子类型。 为什么? 再次,因为复制可能会做坏事,也就是说可能会尝试写一个String
到from参数中,如果我们实际上传递了一个Int的数组,那么ClassCastException将在稍后抛出。
那么,我们唯一要确保的是copy()不会做任何坏事 我们要禁止它向from参数中写,我们可以:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
这里发生的事情就是类型推测:我们说,from不仅仅是一个数组,而是一个限制的(可推测)数组:我们只能调用那些返回类型参数T的方法,在这种情况下,这意味着我们只能调用get()
。 这是我们使用使用点分歧,对应于Java的Array <? extends Object>
,但是稍微简单一点。
你也可以使用in来推测一个类型:
fun fill(dest: Array<in String>, value: String) {
// ...
}
Array <in String>
对应于Java Array <? super String>
,即可以将一个CharSequence数组或一个Object数组传递给fill()
函数。
星号推测(Star-projections)
有时你想说,你对这个类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方法是定义一个通用类型的推测,即该通用类型的每个具体实例都将是该推测的子类型。
Kotlin提供了为此提供了被称为星号推测的语法:
- 对于
Foo <out T>
,其中T是具有上限TUpper的协变类型参数,Foo<*>
相当于Foo <out TUpper>
。 这意味着当T未知时,您可以从Foo <*>
安全地读取TUpper
的值。 - 对于
Foo <in T>
,其中T是逆变类型参数,Foo<*>
相当于Foo <in Nothing>
。 这意味着当T未知时,没有什么可以以安全的方式写入Foo <*>
。 - 对于
Foo <T>
,其中T是具有上限TUpper的不可变类型参数,Foo <*>
等同于用于读取值的Foo <out TUpper>
和用于写入值的Foo <in Nothing>
。
如果泛型具有多个类型参数,则可以独立地推测每个类型参数。 例如,如果类型被声明为interface Function<in T, out U>
,我们可以猜想以下星号预测:
-
Function<*, String>
意味着Function<in Nothing, String>
-
Function<Int, *>
意味着Function<Int, out Any?>
-
Function<*, *>
意味着Function<in Nothing, out Any?>
注意:星号预测非常像Java原始类型,但是安全。
泛型函数(Generic functions)
不仅类可以有泛型。函数也可以。泛型放在函数名称之前:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // extension function
// ...
}
要调用泛型函数,需要在函数名称之后指定类型参数:
val l = singletonList<Int>(1)
泛型约束
所有可能的类型集合可以被替代,
可以替代给定类型参数的所有可能类型的集合可能受到泛型约束的限制。
上限(Upper bounds)
最常见的约束类型是对应于Javaextends
关键字的上限:
fun <T : Comparable<T>> sort(list: List<T>) {
// ...
}
冒号后面指定的类型是上限:只有Comparable<T>
的子类型可以作为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() }
}