
6.5.1 类型不变性
当一个方法接收到一个T类的对象时,你可以传递T的任何派生类的对象。例如,如果你可以传递一个Animal的实例,那么你也可以传递一个Dog的实例,它是Animal的一个子类。但是,如果方法接收到类型为T的泛型对象,例如List<T>,那么你不能传递派生类型为T的泛型对象的对象。例如,如果可以传递List<Animal>,则不能传递List<Dog>,因为Dog扩展了Animal。这就是类型不变性——你不能对类型进行改变。
让我们先用一个例子来说明类型不变性,然后将在这个例子的基础上学习类型变化。假设我们有一个Fruit类以及从它那里继承的两个类:

现在假设一篮子水果由Array<Fruit>表示,我们有一个接收并处理它的方法。

现在,receiveFruits()方法只是打印给定数组的大小。但是,在将来,它可能会更改为从Array<Fruit>中获取一个对象,或者将一个对象设置到Array<Fruit>。现在,如果我们有一篮子香蕉,即Array<Banana>,那么Kotlin会像Java一样,不允许我们将其传递给receiveFruits()方法:

这种限制是由于Kotlin的带有类型不变性的泛型类型——一篮子香蕉不是从一篮子水果继承来的。这是有充分理由的。继承意味着可替换性——也就是说,派生类的实例可以传递给任何需要基类实例的方法。如果允许Array<Banana>的一个实例作为参数传递,而预期的是Array<Fruit>的一个实例,那么如果receiveFruits()方法要向Array<Fruit>中添加一个Orange,我们可能会遇到麻烦。在这种情况下,在处理Array<Banana>时,我们稍后会在试图将Orange实例视为Banana实例时遇到转换异常的错误——orange不喜欢这样的处理。或者,我们可以尝试实现receiveFruits()方法,这样它只在给定参数不是Array<Banana>的情况下添加一个Orange,但是这样的类型检查将会导致违反“Liskov 替换原则”——参见Agile Software Development, Principles, Patterns, and Practices [Mar02]。
尽管Banana继承自Fruit,但是Kotlin不允许将Array<Banana>传递到预期为Array<Fruit>的地方,Kotlin使得泛型类型的使用是安全的。
在继续之前,让我们稍微修改一下代码,以理解最初看起来很古怪的东西,但其实际上是健全类型系统的标志。

我们将参数类型从Array<Fruit>改为List<Fruit>。现在,让我们将List<Banana>的一个实例传递给这个函数:

在此修改之后,Kotlin编译器并不会抱怨。这似乎不公平——语言似乎限制了数组,但没有限制列表。但造成这种行为差异的原因是好的。Array<T>是可变的,但是List<T>是不可变的。你可以向Array<Fruit>添加一个Orange,但是不能向List<Fruit>添加任何东西。这是有道理的,但是你可能会好奇Kotlin是怎么知道如何区分二者的。答案在于这两种类型是如何定义的:类Array<T>与接口List<out E>。List的out就是秘诀。让我们继续深入研究这个问题。