在effective java 2nd中第28条,有对java范型PECS的介绍。
首先,我们看一个在java中经常被我们使用的方法addAll():
为什么在addAll的时候Collection的类型要通过继承范型E来进行限定? 有什么特殊的吗?接着我们再来看一看普通的add方法:
有没有觉得很奇怪?使用add方法的时候又不需要限定类型。
下面我们自己来写一个List进行以下测试,看看如果不限定类型的话会发生什么:
可以看到,编译报错:
Error:(24, 41) java: 不兼容的类型: java.util.List
但是,单个add方法却不会报错。虽然Integer是Number的子类,但Collection
关于与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还是比较熟悉的。