为艺术而技术

协变与逆协变

May 06, 2019

假定A <: B (A是B的子类),如果T[A] <: T[B],那么我们就说T是协变的(covariant)。如果T[B] <: T[A],那么我们说T是逆协变的(contravariant)。如果T[B]和T[A]没有从属关系,我们就说T是非协变的(invariant)。

Java

Java中数组是协变的,意味着下面的代码没有问题,但是你需要自己清楚实际数组放的是什么类型。否则运行时可能要出问题。

Object testObj = null;
String[] arrayB = { "a", "b", "c" };
Object[] arrayA = arrayB;
testObj = arrayA[0];

Java中的Generic是Java5引入的,默认是invariant的。 MyClass<String>不是MyClass<Object>的子类或者父类。同样原因,下面的代码会有编译错误。

List<Car> cars = new ArrayList<>();
List<Vehicle> garage = cars;        // compilation error

Java使用use-site方式来实现协变或者逆协变。也就是在使用的时候才知道。这依赖于Java5中引入的wildcard

public void process(List<? extends Car> list) { ... }

也就是说这个List参数变成协变的了,因为它接受所有Car以及Car子类的List。

与之相对,

public void process(List<? super Car> list) { ... }

这个参数就是逆协变了。

Scala

Scala可以使用和Java同样的use-site方式来实现协变或逆协变。

A :< B
L[A] :< L[_ <: B]   // covariant
L[A] :> L[_ >: B]   // contravariant

Scala还可以使用delcare-site方式来实现。

A :< B
L[A] :< L[+B]      // covariant
L[A] >: L[-B]      // contravariant

Producer Extends, Consumer Super (PECS)

先说结论:
producer === extends === read only === covariant
consumer ==== super ==== write only === contravariant

从Java集合的角度, 从集合中取数据就是producer,你就要用extends;向里面存就是consumer,需要使用super。

List<? extends Vehicle> garage = new ArrayList<>();
garage.add(new Vehicle());  // compilation error
garage.add(new Car());      // compilation error
garage.add(new Bus());      // compilation error
// reading behavior
Vehicle vehicle = garageB.get(1);

上面这个例子用的是extends,所以只读,不可写。为什么呢,很简单,因为编译器不知道你到底是哪个子类型,所以不让写。但是你读出来的肯定是Vehicle的子类,所以完全没问题。

List<? super Car> garage = new ArrayList<>();
garage.add(new BMW());
garage.add(new Alto());
garage.add(new Vehicle());    // compilation error
// reading behavior
Object object = garage.get(0);    // I don't get a Car, why?

同样,上面的例子可以写入,但是由于不知道具体类型,所以只能给出最高级类型也就是Object。

从Scala的角度,参数就是Consumer,所以应该是逆协变,而返回值就是Producer,所以就是协变。这样理解下面的Signature就容易了。

trait Function1[-T, +R] extends AnyRef
trait Function2[-T1, -T2, +R] extends AnyRef

Scala支持高阶函数,因此函数本身也就有了范型。

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
abstract class SmallAnimal extends Animal
case class Mouse(name: String) extends SmallAnimal

基于上面的解释,Animal是Cat的父类,而Mouse是SmallAnimal的子类,因此我们可以说 Animal => MouseCat => SmallAnimal 的子类型。

致谢

https://medium.com/@sinisalouc/variance-in-java-and-scala-63af925d21dc

https://docs.scala-lang.org/tour/variances.html

https://medium.com/@isuru89/java-producer-extends-consumer-super-9fbb0e7dd268


Qingfei Yuan

Written by Qingfei Yuan who builds useful things.

© 2019 - 2020 yuanqingfei
Creative Commons License