在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还是比较熟悉的。