scala协变逆变与java中PECS的关系

在effective java 2nd中第28条,有对java范型PECS的介绍。

首先,我们看一个在java中经常被我们使用的方法addAll():

为什么在addAll的时候Collection的类型要通过继承范型E来进行限定? 有什么特殊的吗?接着我们再来看一看普通的add方法:

有没有觉得很奇怪?使用add方法的时候又不需要限定类型。

下面我们自己来写一个List进行以下测试,看看如果不限定类型的话会发生什么:

可以看到,编译报错:

Error:(24, 41) java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.Collection<java.lang.Number>

但是,单个add方法却不会报错。虽然Integer是Number的子类,但Collection<Integer>却不是Collection<Number>的子类,原因在此。

关于与addAllForTest对应的popAll方法,各位可以自己试一下。

PECS表示producer-extends、consumer-super,在上面的例子中,producer即addAllForTest,consumer是需要你实现的popAll,而extends与super则是针对这两个方法中参数的范型而言的。

下面介绍scala中的协变逆变:

这里写图片描述

这里写图片描述

进行声明时,用[+T]表示协变,[-T]表示逆变。

协变:如果String是AnyRef的子类,那么List[String]也是List[AnyRef]的子类。

逆变:如果String是AnyRef的子类,那么List[String]则是List[AnyRef]的父类。

协变点:方法返回值的位置称为协变点(covariant position)。

逆变点:方法参数的位置称为做逆变点(contravariant position)。

下面给一段代码加上注释来进行说明:

  // 声明协变,但会报错
  // covariant type A occurs in contravariant position in type A of value x
  // 协变类型A不允许出现在逆变点
  class Person[+A]{
    /** 假设该方法通过编译,那么pAnyref = pString之后,继续调用pAnyref.test(123)便会报错
      * 因为pString.test的参数为String类型,但pAnyref的test方法参数类型为Anyref类型
      * 这样一来,pAnyref = pString之后执行pAnyref.test(123)会报错,因为实际运行时是pString在运行
      */
    def test(x: A) = println(x)
  }
  var pAnyref = new Person[AnyRef]
  var pString = new Person[String]
  pAnyref = pString

这个例子会在def test(x: A) 处报错,无法进行编译。下面是逆变的例子:

  // 声明逆变,下面这行代码会编译出错
  // contravariant type A occurs in covariant position in type  A of value test
  // 逆变类型A不允许出现在协变点上
  class Person[-A] {
    /** 假设该方法通过编译,那么pString = pAnyref之后,继续调用pString.test便会报错
      * 因为pAnyref.test返回Anyref,而pString作为父类,返回值为String
      * 在pString = pAnyref之后,调用pString.test的话,返回的其实是Anyref,与pString.test应有的返回值String不匹配,发生报错
      */
    def test: A = null.asInstanceOf[A]
  }
  var pAnyref = new Person[AnyRef]
  var pString = new Person[String]
  pString = pAnyref

对于scala中协变逆变的使用,是可以同java中的PECS互相参考的,这样学习起来变回容易很多,毕竟大家对java还是比较熟悉的。

猜你喜欢

转载自zk-chs.iteye.com/blog/2293382