Java 数组

先来看一个 Java 中的例子,Java 中的数组是协变的。也就是说,一个 String 数组(String[])是可以被当成 Object 数组(Object[])处理的:

String[] a1 = { "abc" };
Object[] a2 = a1;

这种协变虽然在读取数组内容时不会有问题(a1 数组中的 String 元素可以被当成 Object 使用),但是修改数组内容时就会出现无法在编译期检测出来的错误了:

a2[0] = new Integer(17)
String s  = a1[0]  // java.lang.ArrayStoreException

之所以要采用这种设计,Java 的发明者 James Gosling 曾解释说,这样做就能用一种简单通用的方式处理 Java 数组了。例如 java.util.Arrays 提供了sort 方法用于所有数组类型的排序,它的函数声明是 sort(Object[] a, Comparator c),如果 Java 数组不支持协变,那么就很难简单的写出这样通用的排序方法了。

Scala 中的协变和逆变

wikipedia 上关于[协变和逆变的解释](http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)是:

Within the type system of a programming language, covariance and contravariance refers to the ordering of types from narrower to wider and their interchangeability or equivalence in certain situations (such as parameters, generics, and return types).

简单来说,它们指定了不同类型相互之间的可转换性。协变类型可以从较普通的类(动物)转换到更精细的类(猫),而逆变则允许从较精细的类(三角形)转换到较普通的类(几何图形)。

Scala 中默认的参数类型是不变(invariant)的,也就是说,下面代码中 Invariant[Object] 类和 Invariant[String] 类的实例,均无法转化成另外一种类型。

scala> class Invariant[T]
defined class Invariant

scala> var x: Invariant[Object] = new Invariant[Object]
x: Invariant[java.lang.Object] = [email protected]

scala> var x: Invariant[Object] = new Invariant[String]
<console>:8: error: type mismatch;
 found   : Invariant[String]
 required: Invariant[java.lang.Object]
                                  
scala> var x: Invariant[String] = new Invariant[Object]
<console>:8: error: type mismatch;
 found   : Invariant[java.lang.Object]
 required: Invariant[String]

在 Scala 中,如果要把某一参数类型 T 声明为协变,只需要在它的前面加上 + 号即可:

scala> class Covariant[+T]
defined class Covariant

scala> var x: Covariant[Object] = new Covariant[String]
x: Covariant[java.lang.Object] = [email protected]

类似的,声明 T 为逆变的方式是在它前面加上 - 号。

判断一个类型是逆变、协变还是不变的方法,被称为里氏替换原则(Liskov Substitution Principle)。LSP 指出,如果所有类型 U 出现的地方都能用类型 T 替换,那么 T 就可以被认为是 U 的一个子类型。

协变和逆变有时候可以同时作用在一个类型上,比较经典的一个例子就是 Scala 中的 Function1。当出现类似 A => B 的 lambda 函数时,编译器会自动将它转成一个 Function1[A, B] 的定义。标准库中 Function1 的定义如下:

{% codeblock lang:scala %} trait Function1[-S, +T] { def apply(x: S): T } {% endcodeblock %}

这里 S 是函数参数,T 是函数的返回类型。不难理解,当一个函数 f 能替换另一个函数 g 时,f 接受的参数必须是 g 的父类,而 f 的返回结果必须是 g 的返回结果的子类。因此这里 S 是逆变的,而 T 则是协变的。

参考: