取自: https://www.kotlincn.net/docs/reference/generics.html
泛型
与java类似,kotlin中的类也可以有类型参数:
class Box<T>(t: T){
var value = t
}
一般来说,要创建这样类的实例,我们需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
但是如果类型参数可以推断出来,例如从构造函数的参数或者其他途径,允许省略类型参数:
val box = Box(1) //1具有类型Int,所以编译器知道我们说的是Box<Int>。
型变
java类型系统中最棘手的部分之一就是通配符类型。而kotlin中没有。相反,它有两个其他的东西:声明出型变 (declaration-site variance) 与类型投影(type projections)。
首先,让我们思考为什么java需要那些神秘的通配符。在《Effective Java》第三版解释了该问题——第31条:利用有限制通配符来提升API的灵活性。 首先,Java中的泛型是不型变的
,这意味着 List<String>
并不是 List<Object>
的子类型。为什么会这样?如果 List是 不型变的
,它就没比Java的数组好到哪去,因为如下代码会通过编译然后导致运行时异常:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串
因此,Java禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如,考虑 Collection
接口中的 addAll()
方法。该方法的签名应该是什么?直觉上,我们会这样:
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}
但随后,我们将无法做到以下简单的事情(这是完全完全):
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}
在Java中,我们艰难的学到了这个教训,参见《Effective Java》第三版,第28条:列表优先于数组
这就是为什么 addAll()
的实际签名时以下这样:
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
通配符类型参数 ? Extends E
表示此方法接受E
或者E
的一些子类型对象的集合,而不只是E
自身。这意味着我们可以安全地从其中(该集合中的元素时E的子类的实例)读取 E
,但是不能写入,因为我们不知道什么对象符合那个未知的E
的子类型。反过来,该限制可以让Collection<String>
表示为 Collection< ? Extends Object>
的子类型。简而言之,带extends限定(上界)的通配符类型是的类型时协变的(covariant)。
理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用String
的集合,并且从其中读取Object
也没问题。反过来,如果只能向集合中放入项目,就可以用Object
集合并向其中放入String
:在Java中有List<? super String>
是 List<Object>
的一个超类。性
后者成为逆变性(contravariance),并且对于List<? supper String>
你只能调用接受String作为参数的方法(例如,你可以调用add(String)
或者set(int,String)
),当然如果调用函数返回List<T>
中的T
,你得到的并非一个String
而是一个 Object
。
Joshua Bloch称那些你只能从中 读取 的对象为 生产者 ,并称那么你只能 写入 的对象为 消费者 。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型 ”,并提出了以下助记符:
PECS 代表生产者-Extens,消费者-Super (Producer-Extends, Consumer-Super)。
注意:如果你使用一个生产者对象,如 List<? Extends Foo>
,在该对象上不允许调用 add()
或 set()
。但着并不意味着该对象是 不可变的 : 例如,没有什么阻止你调用 clear()
从列表中删除所有项目,因为 clear()
根本无需任何参数。通配符(或者其他类型的型变)保证的唯一的事情是 类型安全 。不可变性完全是另一回事。
声明处型变
假设有一个泛型接口 Source<T>
,该接口中不存在任何以T
作为参数的方法,知识方法返回 T
类型值:
//Java
interface Source<T> {
T next();
}
那么,在 Source<Object>
类型的变量中存储 Source<String>
实例的引用是极为安全的——没有消费者-方法可以调用。但是Java并不知道这一点,并且仍然禁止这样操作:
//Java
void demo(Source<String> strs>{
Source<Object> objects = strs; //!!! 在Java中不允许
//...
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>
,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
在Kotlin中,有一种方法向编译器解释这种情况。这称为声明处型变: 我们可以标注Source
的 类型参数 T
来确保它仅从 Source<T>
成员中返回(生产),并从不被消费。 为此,我们提供out修饰符:
interface Source<out T> {
fun nextT():T
}
fun demo(strs: Source<Sting>) {
val objects: Source<Any> = strs //这个没问题, 因为 T 是一个 out-参数
}
一般原则是:当一个类 C
的类型参数 T
被声明为out时,它就只能出现在C
是T
的 生产者,而不是 T
的消费者。
out修饰符成为型变注解,并且由于它在类型参数声明处提供,所以我们称之为声明处型变。这与Java的使用处型变相反,其类型用途通配符是的类型协变。
另外除了out,Kotlin又补充了一个型变注释: in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类型的一个很好的例子是Comparable
:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) //1.0拥有类型 Double,它是Number的子类型
//因此,我们可以将x 赋给类型为 Comparable <Double>的变量
val y: Comparanle<Double> = x //ok
}
我们相信 in 和 out 两词是自解释的(因为它们已经在C# 中成功使用很长时间了),因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:
**存在性(The Existential)转换: 消费者 in,生产者out!
类型投影
使用处型变:类型投影
将类型参数T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能 限制为只返回T
! 一个很好的例子是Array:
class Array<T>(val size: Int) {
fun get(index: Int): T {...}
fun set(index: Int, value: T) {...}
}